Pomyślałem, że napiszę krótką (dla mnie jest to krótka) „odpowiedź”, aby móc podsumować swoje punkty.
Kilka „najlepszych praktyk” podczas tworzenia systemu przechowywania plików. Przechowywanie plików to szeroka kategoria, więc w przypadku niektórych z nich przebieg może się różnić. Potraktuj je jako sugestię dotyczącą tego, co znalazłem.
Nazwy plików Nie przechowuj pliku pod nazwą nadaną przez użytkownika końcowego. Mogą i będą używać wszelkiego rodzaju gównianych postaci, które uczynią twoje życie nieszczęśliwym. Niektóre mogą być tak złe jak '
pojedyncze cudzysłowy, które w Linuksie w zasadzie uniemożliwiają odczytanie, a nawet usunięcie pliku (bezpośrednio). Niektóre rzeczy mogą wydawać się proste, na przykład spację, ale w zależności od tego, gdzie go używasz i systemu operacyjnego na serwerze, możesz skończyć z
one%20two.txt
lub one+two.txt
lub one two.txt
co może, ale nie musi, powodować wszelkiego rodzaju problemy w twoich linkach.
Najlepszą rzeczą do zrobienia jest utworzenie skrótu, takiego jak sha1
może to być tak proste, jak {user_id}{orgianl_name}
Nazwa użytkownika zmniejsza prawdopodobieństwo kolizji z nazwami plików innych użytkowników.
Wolę robić file_hash('sha1', $contents)
w ten sposób, jeśli ktoś prześle ten sam plik więcej niż raz, możesz go złapać (zawartość jest taka sama, hash jest taki sam). Ale jeśli spodziewasz się dużych plików, możesz wykonać na nim testy porównawcze, aby zobaczyć, jaki rodzaj wydajności ma. Przeważnie zajmuję się małymi plikami, więc działa to dobrze.-zauważ, że ze znacznikiem czasu plik można nadal zapisać, ponieważ pełna nazwa jest inna, ale sprawia, że jest dość łatwy do zobaczenia i można go zweryfikować w bazie danych.
Niezależnie od tego, co zrobisz, poprzedziłbym go znacznikiem czasu time().'-'.$filename
. Jest to przydatna informacja, ponieważ jest to bezwzględny czas utworzenia pliku.
Jeśli chodzi o nazwę to użytkownik podaje plik. Po prostu zapisz to w rekordzie bazy danych. W ten sposób możesz pokazać im nazwę, jakiej oczekują, ale użyj nazwy, o której wiesz, że jest zawsze bezpieczne dla linków.
$filename ='trochę gówna^ fileane.jpg';
$ext = strrchr($filename, '.');
echo "\nExt: {$ext}\n";
$hash = sha1('some crapy^ fileane.jpg');
echo "Hash: {$hash}\n";
$time = time();
echo "Timestamp: {$time}\n";
$hashname = $time.'-'.$hash.$ext;
echo "Hashname: $hashname\n";
Wyjścia
Ext: .jpg
Hash: bb9d2c2c7c73bb8248537a701870e35742b41c02
Timestamp: 1511853063
Hashname: 1511853063-bb9d2c2c7c73bb8248537a701870e35742b41c02.jpg
Możesz spróbować tutaj
Ścieżki nigdy nie przechowuj pełnej ścieżki do pliku. Wszystko, czego potrzebujesz w bazie danych, to skrót od utworzenia nazwy skrótu. Ścieżka "root" do folderu, w którym jest przechowywany plik, powinna być wykonana w PHP. Ma to kilka zalet.
- uniemożliwia przenoszenie katalogów. Ponieważ nie omijasz żadnej części ścieżki wokół siebie, nie musisz się tak bardzo martwić, że ktoś poślizgnie się
\..\..
tam i w miejscach, do których nie powinni. Kiepskim tego przykładem może być ktoś nadpisujący.htpassword
pliku, przesyłając plik o nazwie tak z katalogiem poprzecznym w nim. - Ma bardziej jednolicie wyglądające linki, jednolity rozmiar, jednolity zestaw znaków.
https://en.wikipedia.org/wiki/Directory_traversal_attack
- Konserwacja. Zmieniają się ścieżki, zmieniają się serwery. Zmieniają się wymagania dotyczące Twojego systemu. Jeśli potrzebujesz przenieść te pliki, ale zachowałeś do nich absolutną pełną ścieżkę w bazie danych, utknąłeś sklejając wszystko razem za pomocą
symlinks
lub aktualizować wszystkie swoje rekordy.
Są od tego wyjątki. Jeśli chcesz je przechowywać w miesięcznym folderze lub według nazwy użytkownika. Możesz zapisać tę część ścieżki w osobnym polu. Ale nawet w takim przypadku można by go zbudować dynamicznie na podstawie danych zapisanych w rekordzie. Uważam, że najlepiej jest zapisać jak najmniej informacji o ścieżce. I tworzą konfigurację lub stałą, której możesz użyć we wszystkich miejscach, w których musisz umieścić ścieżkę do pliku.
Również path
i link
są bardzo różne, więc zapisując tylko nazwę, możesz połączyć ją z dowolną stroną PHP bez konieczności odejmowania danych ze ścieżki. Zawsze uważałem, że łatwiej jest dodać do nazwy pliku niż odjąć od ścieżki.
Baza danych (tylko kilka sugestii, zastosowanie może się różnić )Jak zawsze w przypadku danych zadaj sobie pytanie, kto, co, gdzie, kiedy
- identyfikator -
int
automatyczne przyrosty klucza głównego - identyfikator_użytkownika -
int
klucz obcy, kto przesłał go - hasz -
char[40] *sha1*, unique
co hasz - hashname -
varchar
{timestampl}-{hash}.{ext} gdzie nazwa pliku na dysku twardym - nazwa pliku -
varchar
oryginalna nazwa podana przez użytkownika, w ten sposób możemy pokazać mu nazwę, jakiej oczekuje (jeśli to ważne) - stan -
enum[public,private,deleted,pending.. etc]
status pliku, w zależności od przypadku użycia, może być konieczne przejrzenie plików, a może niektóre są prywatne, tylko użytkownik może je zobaczyć, może niektóre są publiczne itp. - data_stanu -
timestamp|datetime
czas zmiany statusu. - data_utworzenia -
timestamp|datetime
kiedy kiedy plik został utworzony, preferowany jest znacznik czasu, ponieważ ułatwia to niektóre rzeczy, ale w takim przypadku powinien to być ten sam znacznik czasu w nazwie skrótu. - typ -
varchar
- typ mimu, może być przydatny do ustawiania typu mimu podczas pobierania itp.
Jeśli oczekujesz, że różni użytkownicy prześlą ten sam plik i użyjesz file_hash
możesz zrobić hash
pole połączony unikalny indeks user_id
i hash
w ten sposób konflikt byłby tylko wtedy, gdyby ten sam użytkownik przesłał ten sam plik. Możesz to również zrobić w oparciu o znacznik czasu i hash, w zależności od potrzeb.
To podstawowe rzeczy, o których mogłem pomyśleć, to nie jest absolutne tylko niektóre dziedziny, które moim zdaniem byłyby przydatne.
Przydatne jest posiadanie samego skrótu, jeśli przechowujesz go samodzielnie, możesz go zapisać w CHAR(40)
for sha1 (zajmuje mniej miejsca w DB niż VARCHAR
) i ustaw sortowanie na UTF8_bin
który jest binarny. To sprawia, że w wyszukiwaniu rozróżniana jest wielkość liter. Chociaż prawdopodobieństwo kolizji skrótów jest niewielkie, zapewnia to nieco większą ochronę, ponieważ skróty są pisane dużymi i małymi literami.
Zawsze możesz zbudować hashname
w locie, jeśli przechowujesz rozszerzenie i sygnaturę czasową osobno. Jeśli odkryjesz, że tworzysz coś od czasu do czasu, możesz po prostu przechowywać to w bazie danych, aby uprościć pracę w PHP.
Lubię po prostu umieszczać hash w linku, bez rozszerzenia, nic, więc moje linki wyglądają tak.
http://www.example.com/download/ad87109bfff0765f4dd8cf4943b04d16a4070fea
Naprawdę proste, naprawdę ogólne, bezpieczne w adresach URL, zawsze w tym samym rozmiarze itp.
hashname
dla tego "pliku" wyglądałby tak
1511848005-ad87109bfff0765f4dd8cf4943b04d16a4070fea.jpg
Jeśli masz konflikty z tym samym plikiem i innym użytkownikiem (o czym wspomniałem powyżej). Zawsze możesz dodać część sygnatury czasowej do linku, identyfikator_użytkownika lub oba. Jeśli używasz user_id, przydatne może być lewe uzupełnienie go zerami. Na przykład niektórzy użytkownicy mogą mieć ID:1
a niektóre mogą być ID:234
więc możesz zostawić go do 4 miejsc i zrobić z nich 0001
i 0234
. Następnie dodaj to do skrótu, co jest prawie niezauważalne:
1511848005-ad87109bfff0765f4dd8cf4943b04d16a4070fea0234.jpg
Ważną rzeczą tutaj jest to, że ponieważ sha1
to zawsze 40
a id to zawsze 4
możemy je dokładnie i łatwo oddzielić. I w ten sposób nadal możesz to wyjątkowo sprawdzić. Istnieje wiele różnych opcji, ale wiele zależy od Twoich potrzeb.
Dostęp Takich jak pobieranie. Powinieneś zawsze wyprowadzać plik z PHP, nie dawaj im bezpośredniego dostępu do pliku. Najlepszym sposobem jest przechowywanie plików poza webrootem ( nad public_html
lub www
teczka ). Następnie w PHP możesz ustawić właściwy typ nagłówków i po prostu odczytać plik. Działa to prawie we wszystkim z wyjątkiem wideo. Nie zajmuję się filmami, więc to temat poza moim doświadczeniem. Ale uważam, że najlepiej jest myśleć o tym, ponieważ wszystkie dane pliku to tekst, jego nagłówki, które przekształcają ten tekst w obraz, plik Excela lub pdf.
Wielką zaletą nie dawania im bezpośredniego dostępu do pliku jest to, że jeśli masz witrynę członkowską, jeśli nie chcesz, aby zawartość była dostępna bez logowania, możesz łatwo sprawdzić w PHP, czy są zalogowani, zanim udostępnisz im zawartość. A ponieważ plik znajduje się poza webrootem, nie mogą uzyskać do niego dostępu w żaden inny sposób.
Najważniejszą rzeczą jest wybranie czegoś spójnego, co jest wciąż wystarczająco elastyczne, aby sprostać wszystkim Twoim potrzebom.
Jestem pewien, że mogę wymyślić więcej, ale jeśli masz jakieś sugestie, możesz je skomentować.
PODSTAWOWY PRZEBIEG PROCESU
- Użytkownik przesyła formularz (
enctype="multipart/form-data"
)
https://www.w3schools.com/tags/att_form_enctype.asp
- Serwer odbiera wiadomość z formularza, Super Globals
$_POST
i$_FILES
http://php.net/manual/en/reserved.variables.files .php
$_FILES = [
'fieldname' => [
'name' => "MyFile.txt" // (comes from the browser, so treat as tainted)
'type' => "text/plain" // (not sure where it gets this from - assume the browser, so treat as tainted)
'tmp_name' => "/tmp/php/php1h4j1o" // (could be anywhere on your system, depending on your config settings, but the user has no control, so this isn't tainted)
'error' => "0" //UPLOAD_ERR_OK (= 0)
'size' => "123" // (the size in bytes)
]
];
-
Sprawdź błędy
if(!$_FILES['fielname']['error'])
-
Oczyść wyświetlaną nazwę
$filename = htmlentities($str, ENT_NOQUOTES, "UTF-8");
-
Zapisz plik, utwórz rekord DB (PSUDO-CODE)
Tak:
$path = __DIR__.'/uploads/'; //for exmaple
$time = time();
$hash = hash_file('sha1',$_FILES['fielname']['tmp_name']);
$type = $_FILES['fielname']['type'];
$hashname = $time.'-'.$hash.strrchr($_FILES['fielname']['name'], '.');
$status = 'pending';
if(!move_uploaded_file ($_FILES['fielname']['tmp_name'], $path.$hashname )){
//failed
//do somehing for errors.
die();
}
//store record in db
http://php.net/manual/en/function.move -przesłany-plik.php
-
Utwórz link ( różni się w zależności od routingu ), najprościej jest to zrobić w ten sposób
http://www.example.com/download?file={$hash}
ale jest brzydszy niżhttp://www.example.com/download/{$hash}
-
użytkownik klika link prowadzi do strony pobierania.
pobierz INPUT i wyszukaj rekord
$hash = $_GET['file'];
$stmt = $PDO->prepare("SELECT * FROM attachments WHERE hash = :hash LIMIT 1");
$stmt->execute([":hash" => $hash]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($row);
http://php.net/manual/en/intro.pdo.php
Itd....
Pozdrawiam!