PHP: How do you get a file's extension?
There is a built-in function for the file extension, and it is the one to use. The short home-made variants you find alongside it return something wrong in two very everyday cases.
The answer
<?php
$extension = pathinfo($filename, PATHINFO_EXTENSION);
?>
That is the way. Here is what comes out of it:
bild.jpg -> 'jpg'
archiv.tar.gz -> 'gz'
LIESMICH.TXT -> 'TXT'
ohne_endung -> '' (empty)
.htaccess -> 'htaccess'
punkt.am.ende. -> '' (empty)
/pfad/zu/datei.php -> 'php'
ordner.mit.punkt/datei -> '' (empty)
Two lines need explaining. For archiv.tar.gz you get gz and not tar.gz —
nothing in PHP knows about multi-part extensions, because the file system does
not know about them either. And for .htaccess the whole name counts as the
extension, because there is nothing in front of it.
Why not to home-build it
The widespread alternatives side by side:
substr(strrchr) explode/end pathinfo
bild.jpg 'jpg' 'jpg' 'jpg'
ohne_endung '' 'ohne_endung' ''
.htaccess 'htaccess' 'htaccess' 'htaccess'
archiv.tar.gz 'gz' 'gz' 'gz'
ordner.mit.punkt/datei 'punkt/datei' 'punkt/datei' ''
Two rows differ.
For ohne_endung, explode/end returns the entire file name. That is
the nastier of the two bugs, because a non-empty string comes back — so your
code does not notice anything went wrong, and you create a file with the
extension ohne_endung.
For ordner.mit.punkt/datei, strrchr() and explode() find a dot that
belongs to the directory. pathinfo() knows it is looking at a path and splits
correctly. Versioned directories like v1.2/ or domains used as folder names
are not rare.
The case you hit immediately when handling URLs
If you pull the extension out of a URL, this happens:
pathinfo('https://example.com/bild.jpg?v=2&x=1', PATHINFO_EXTENSION)
-> 'jpg?v=2&x=1'
The query string comes along with it. pathinfo() cannot know about that, it
only sees a string with a dot in it. The way around:
<?php
$extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
?>
via parse_url : 'jpg'
A fragment (#something) would be picked up just the same, by the way.
parse_url() clears both away.
pathinfo() without the constant
Without the second parameter you get an array. And the extension key is
missing entirely when there is no extension — it is not empty:
pathinfo('ohne_endung') keys : ["dirname","basename","filename"]
pathinfo('bild.jpg') keys : ["dirname","basename","extension","filename"]
isset($p['extension']) : false
So a direct $p['extension'] raises a Warning: Undefined array key. Either
guard it with ?? or use the constant in the first place — that returns an
empty string in this case.
Upper and lower case
pathinfo() returns the extension exactly as written:
pathinfo('LIESMICH.TXT') : 'TXT'
strtolower of that : 'txt'
If you check against a list of permitted extensions, a strtolower() belongs
in between. Otherwise somebody gets past your check with .JPG — or fails to
get past it although they should.
And now the important part: the extension says nothing about the content
This is the reason I am writing this article at all. A file name is a claim,
not a proof. I named a file containing PHP code harmlos.jpg:
file name : harmlos.jpg
extension per pathinfo : jpg
finfo says : text/x-php
getimagesize() : false
pathinfo() dutifully says jpg. finfo looks inside the file and says
text/x-php. If your upload check only looks at the extension, there is now
executable PHP code sitting in your upload directory.
The reverse happens too — a genuine PNG that somebody named .txt:
extension per pathinfo : txt
finfo says : image/png
getimagesize() : 2 x 2, mime image/png
For uploads that means:
<?php
$fi = new finfo(FILEINFO_MIME_TYPE);
$type = $fi->file($_FILES['file']['tmp_name']);
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
];
if (!isset($allowed[$type])) {
throw new RuntimeException('file type not permitted: ' . $type);
}
// Assign the extension yourself, do not take the user's.
$target = $directory . '/' . bin2hex(random_bytes(16))
. '.' . $allowed[$type];
?>
Three things about this matter. First, the type is determined from the content. Second, the list is an allowlist — whatever is not in it is forbidden. And third, you assign the file name yourself instead of taking the user’s.
What you should not use is $_FILES['file']['type']. That value comes from
the upload request, so from the client, and can be set just as freely as any
other header.
On top of that, the upload directory belongs configured so that nothing is executed there — outside the document root, or with a matching web server rule. Do not rely on the check alone being enough.
Multibyte file names
Briefly, because the question comes up regularly:
Übung.pdf -> pdf
файл.txt -> txt
日本語.md -> md
Works. The dot is unambiguous in UTF-8, and the extension itself is as a rule ASCII.
Summary
pathinfo($name, PATHINFO_EXTENSION)and nothing else.substr(strrchr(...))andexplode/endget it wrong for files without an extension and for dots in directory names.- For URLs,
parse_url(..., PHP_URL_PATH)first, otherwise the query string comes along. - Without the constant the
extensionkey is missing entirely. - Do not forget
strtolower()when checking against a list. - For uploads check the content (
finfo), not the name — and not$_FILES[...]['type'], which comes from the client.
About Netcup (advertisement)
The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).