Szyfrowanie plików w /wp-content/uploads

Stanąłem przed problemem stworzenia zamkniętej sekcji na pewnym WordPressie. W ramach owej zamkniętej sekcji zalogowani użytkownicy mogą dodać lub zobaczyć wpis, który będzie widoczny tylko dla innych zalogowanych użytkowników. Temat nie nowy i przewałkowany na wiele sposobów, więc wiedziałem, że nie sprawi mi to większych problemów.

Problem leżał jednak gdzie indziej. Dodając wpis zalogowani użytkownicy mogli dodać do niego poufny dokument jako załącznik. To też nie jest trudne, jednak jako, że taki dodawany plik lądować miał jak każdy inny standardowy załącznik do wpisu w katalogu /wp-content/uploads, osoby znające WordPressa mogły go sobie stamtąd dość łatwo wyłuskać. Trzeba było te pliki w jakiś sposób ukryć.

Najprościej byłoby po prostu za pomocą .htaccess uniemożliwić otwieranie takich plików osobom które albo nie są zalogowane, albo próbują je pobrać bezpośrednio z katalogu /uploads. Problemem jednak było, że na blogu część wpisów była dotępna dla wszystkich, w tym także i załączniki do takich wpisów. Jak rozróżnić który plik z /wp-content/uploads mogą otwierać wszyscy, a który tylko zalogowani?

Właściwie się nie da. Przez chwilę przeszła mi przez głowę myśl,  aby te specjalne chronione pliki trzymać gdzieś indziej. Chciałem tu jednak do dodawania załączników wykorzystać standardową funkcję media_handle_upload() – pobiera ona zaczep do przesłanego pliku i ID wpisu, którego plik ma być załącznikiem i załatwia za nas całą sprawę: umieszcza pobrany plik w /uploads, generuje dla niego „plikowpis” (każdy załącznik w wordpressie to kolejny post z polem guid wskazującym na położenie załącznika) i niczym nie musimy się przejmować. Problemem jest tu jednak fakt, że nie możemy w prosty sposób zmienić katalogu do którego dodawany jest nasz plik.

Pomyślałem: a może by tak przesłane tak pliki szyfrować? I dodatkowo stworzyć funkcję, która sprawdzi czy osoba jest zalogowana i jeśli tak, to odda plik w postaci zdeszyfrowanej?

Jak pomyślałem, tak zrobiłem. Patrzcie sami:

Formularz dodawania wpisu z załącznikiem

Stworzyłem podstronę, na której (po sprawdzeniu oczywiście czy osoba jest zalogowana i może dodać wpis) wyświetlany był formularz dodawania wpisu. Nie będę niestety dokładnie go przedstawiał (to jest temat na osobny wpis, a ponadto już opisywałem to choćby na pierwszym polskim wordcampie). Miał pole na tytuł wpisu, jego treść… Ważne rzeczy o których należy pamiętać to rozpoczęcie formularza od:

<form method="post" enctype="multipart/form-data">

enctype informuje tutaj, że wraz z formularzem mogą zostać przesłane pliki. Pole na dodanie pliku wygląda tak:

<input type="file" name="async-upload">

Zapisywanie pliku z załącznikiem

Po wysłaniu formularza plik znajduje się w zmiennej tablicowej $_FILES[’async-upload’]. W pierwszej kolejności należy jednak stworzyć wpis podstawowy, do którego załącznikiem będzie ten plik. Służy do tego oczywiście funkcja wp_insert_post($args) przyjmująca tablicę z danymi do wpisu (w naszym wypadku tytułem tego wpisu i treścią). Funkcja ta zwraca identyfikator utworzonego wpisu:

$id =  wp_insert_post($args);

Gdy mamy już ID wpisu, możemy do niego załączyć nasz plik:

$id_wpisopliku = media_handle_upload('async-upload', $id);

Tak, powyższa linijka załatwiła nam większość spraw: dodała załącznik do /uploads, stworzyła dla niego 'wpisoplik’ z wszystkimi danymi o plikui zwróciła identyfikator takiego wpisopliku.

Plik jest dodany, ale jeszcze nie jest zaszyfrowany.

Szyfrujemy plik załącznika

Musimy teraz za pomocą PHP otworzyć taki plik, zapisać jego zawartość w zmiennej (jako ciąg), zaszyfrować tę zmienną, umieścić ponownie w pliku i plik nadpisać. Brzmi na skomplikowane i takie niestety nieco jest. Zobaczmy więc po kolei co się musi wydarzyć:

Musimy ustalić gdzie nasz plik dokładnie się zapisał. Jak wspomniałem wyżej przechowywane to jest jako guid wpisopliku. Niestety guid wskazuje nam na ścieżkę do pliku, zaczynającą się od http://, co może i pozwoli nam otworzyć plik, ale nie pozwoli potem zapisać w nim zmian. Należy zmienić tę ścieżkę na ścieżkę „wewnątrzserwerową” (nie mam pomysłu jak inaczej nazwać uchwyt do pliku znajdującego się na tym samym komputerze, na którym znajduje się skrypt PHP, który plik ten otworzy i przetworzy):

$wpisoplik = get_post($id_wpisopliku);
$katalog_upload = wp_upload_dir();
$gdzieplik = str_replace($katalog_upload['baseurl'], 
             $katalog_upload['basedir'], $wpisoplik->guid);

Po tych trzech zaklęciach mamy już w zmiennej $gdzieplik lokalizację, z której możemy plik otworzyć w celu obróbki. Zatem otwieramy i obrabiamy:

$przeslanyplikcontent = fopen( $gdzieplik, 'r+');
while (!feof($przeslanyplikcontent))
{
$fcontent .= fread($przeslanyplikcontent, 512 * 1024);
}
$fcontent = koduj_ciag($fcontent);
rewind($przeslanyplikcontent);
fwrite($przeslanyplikcontent, $fcontent);
fclose($przeslanyplikcontent);

Powyżej po kolei:

  • otworzyliśmy plik z prawami do odczytu i zapisu (fopen)
  • sczytaliśmy całą zawartość pliku do zmiennej $fcontent (pętla while)
  • zakodowaliśmy zawartość pliku (koduj_ciag())
  • przewinęliśmy wewenętrzny wskaźnik pliku do początku funkcją rewind() (inaczej dalsze operacje zamiast nadpisać plik, dopisałyby jego zakodowaną zawartość do końca zawartości już istniejącej)
  • zapisaliśmy nową zawartość pliku (fwrite) i zwolniliśmy dostęp do pliku (fclose)

Wszystkie powyższe funkcje znajdziecie w dokumentacji języka PHP, jedynie funkcja koduj_ciag to funkcja napisana przeze mnie i wygląda ona tak:

define('FILE_E_KEY', 'tajny_ciąg_%1@');

function koduj_ciag($ciag) {
$ciag = base64_encode(
        mcrypt_encrypt(MCRYPT_RIJNDAEL_256,
               md5(FILE_E_KEY),
               $ciag,
               MCRYPT_MODE_CBC,
               md5(md5(FILE_E_KEY))));
return $ciag;
}

Brzmi skomplikowanie, ale uwierzcie, że nikt, kto nie zna zawartości stałej FILE_E_KEY nie będzie mógł odkodować tak zakodowanego ciągu. Ustawcie ją więc u siebie na jakiś długi, przypadkowy ciąg.

Plik jest już zakodowany! Będzie go można zobaczyć w katalogu /wp-content/uploads, ale bezpośrednie otworzenie spowoduje wyświetlenie albo śmieci, albo komunikatu o błędzie. Oto próba otworzenia tak zakodowanego pliku PDF:

Ten sam plik otworzony notatnikiem (fragment):

Kodowanie dotyczy oczywiście nie tylko plików PDF ale wszelkich innych –  zarówno binarnych jak i tekstowych:

Mała uwaga: w przypadku plików graficznych, wordpress podczas dodawania ich jako załącznik od razu generuje dla nich miniaturki. Jeśli chcemy by też zostały zakodowane, należy je także przepuścić przez powyższą funkcję.

Tworzymy link do zakodowanego pliku

Od tej pory podanie naturalnego odnośnika do pliku zakończy się jak powyżej. Musimy więc odnośniki do takich plików podawać jako na przykład:

<a href="example.com/sciezka/att.php?a=123">otwórz plik</a>

Ścieżka taka wskazuje na plik att.php, który za chwilę utworzymy i który na bazie parametru a wyświetla odpowiedni plik (wcześniej sprawdzając czy osoba, która chce go zobaczyć ma do tego prawo i jeśli tak to dekodując zawartość.

Plik dekodujący

Musimy utworzyć na naszym serwerze plik att.php, a oto jego zawartość.

W pierwszej kolejności wczytujemy do niego wordpressowe api (choćby po to by na bazie zmiennej 'a’ sprawdzić co to za plik i czy osoba, która łączy się z tym plikiem jest zalogowanym userem wordpressa):

<?php define('WP_USE_THEMES', false); ?>
<?php require('../../../../wp-blog-header.php');

Ścieżka w require może u Was wyglądać nieco inaczej. Na pewno musi wskazywać na plik wp-blog-header.php, który jest w głównym katalogu instalacji wordpressa.

Sprawdzamy teraz czy osoba otwierająca plik ma do tego prawo. Oto jak wyglądałby ten kod ograniczający dostęp tylko do administratorów bloga:

if (!current_user_can("administrator")) {
exit("pliki pobierać mogą tylko zalogowani użytkownicy");
}

Teraz sprawdzamy czy parametr 'a’ jest liczbą:

$id = $_GET['a'];

if (!is_numeric($id)) {
exit("Podany identyfikator pliku do pobrania jest nieprawidłowy");
}

i pobieramy wpisoplik o tym parametrze:

$wpisoplik = get_post($id);

Tak jak i wcześniej potrzebna nam będzie lokalizacja pliku:

$katalog_upload = wp_upload_dir();
$gdzieplik = str_replace($katalog_upload['baseurl'], 
             $katalog_upload['basedir'], $wpisoplik->guid);

Potrzebujuemy też nazwę pliku pod jaką chcemy plik odesłać, a jako, że ta jeest ostatnim członem po ukośniku w guid plikowpisu:

$nazwaarray = explode("/", $wpisoplik->guid);
$nazwa = urlencode(end($nazwaarray));

Mamy już wszystko co nam trzeba. Zaczynamy od przesłania nagłówków do przeglądarki mówiących o tym jaki to jest plik i aby przeglądarka spróbowała go zapisać:

header('Content-Disposition: attachment; filename=' . $nazwa);
header('Content-Type: ' . $wpisoplik->post_mime_type);

I teraz pojawia się podobny zapis jak wcześniej:

$przeslanyplikcontent = fopen( $gdzieplik, 'r');
while (!feof($przeslanyplikcontent))
{
$content .= fread($przeslanyplikcontent, 512 * 1024);
}
$content = dekoduj_ciag($content);
echo $content;
fclose($przeslanyplikcontent);
exit();

Tyle, że teraz:

  • otwieramy plik (fopen)
  • pobieramy jego zawartość (pętla while)
  • dekodujemy zawartość (funkcja dekoduj_ciag)
  • zamiast zapisywać nową zawartość, za pomocą echo wysyłamy ją do przeglądarki
  • i kończymy całą zabawę (fclose i exit)

Jak wygląda funkcja dekoduj_ciag? Cieszę się, że pytacie, bo bez niej całość nie miała by sensu:

function dekoduj_ciag($ciag) {
$ciag = rtrim(
        mcrypt_decrypt(MCRYPT_RIJNDAEL_256,
                       md5(FILE_E_KEY),
                       base64_decode($ciag),
                       MCRYPT_MODE_CBC,
                       md5(md5(FILE_E_KEY))), "\0");
return $ciag;
}

I to wszystko. Mam nadzieję, że przyda się części z Was, a jeśli nie, to przynajmniej dzięki temu wpisowi dowiedzieliście się coś nowego o WordPressie i samym języku PHP ;)

Jeśli ktoś ma pomysły na ulepszenia, albo inne rozwiązania tego problemu, zapraszam do komentarzy!


Opublikowano

w

,

przez

Komentarze

8 odpowiedzi na „Szyfrowanie plików w /wp-content/uploads”

  1. Awatar Daggerka

    Fascynujące ;-)

  2. Awatar Zotrec
    Zotrec

    Hmmm w przypadku dużego serwisu możemy zbytnio obciążyć serwer.

    1. Awatar Konrad Karpieszuk

      niestety zgadza sie

      1. Awatar Zotrec
        Zotrec

        A możeby tak użyć wp handle upload

        „Handle PHP uploads in WordPress, sanitizing file names, checking extensions for mime type, and moving the file to the appropriate directory within the uploads directory. „

  3. Awatar rwpb

    A nie prościej było by taki plik przepuścić przez rara albo zipa i dodać hasło? Dobre hasło z narodowymi literami to sporo czasu na jego odczytanie przy dobrym sprzęcie. Podanie hasła do pliku dla uprawnionych użytkowników też nie stanowi problemu. I obciążenie serwera mniejsze.

    1. Awatar Konrad Karpieszuk

      pomyslow jest wiele :) w wypadku z zipem z haslem narazamy sie na to, ze w sieci pojawi sie gdzies odnosnik do tego pliku wraz z zamieszczonym haslem. i wtedy nie bedziemy nawet wiedziec kto i kiedy pobiera :)

  4. Awatar Artur

    Dzięki za napisanie artykułu, przydał mi się troszeczkę w jednym z moich projektów. Jednak zauważyłem jeden błąd. Pliki docx po odszyfrowaniu nie działają poprawnie i Word musi je naprawiać. Nie wiem jak ominąć ten problem w normalny sposób ponieważ nie wiem gdzie problem leży. Znalazłem w internecie sposób który tymczasowo jest dla mnie dobry. Ciąg przed szyfrowaniem trzeba spakować gzcompress( $ciag ) a po rozszyfrowaniu rozpakować gzuncompress( $ciąg ). Oszczędzamy w ten sposób trochę miejsca na serwerze i mamy pewność, że pliki docx otwierają się poprawnie. Jest to dodatkowe obciążenie dla serwera ale innego rozwiązania nie znalazłem.