I think everyone remembers their first major fumble. I haven't deleted a database yet, though I did accidentally delete an app server that I had to rebuild. This is a story about my first vulnerability that I wrote. I was a student working for my college IT department and recently had gotten promoted to a developer/sysadmin/database administrator position that had been vacant for about a year. I inherited a suite of PHP applications, mostly custom webpages and forms for the library catalog and staff.
One of those applications was an app to show the new library acquisitions: new books, journals, DVDs, etc. The marketing department was redesigning the library website, and since there was now someone on staff to update the custom pages (me), the library staff wanted the custom page to match the style of the new website. They also asked for a few other features, like importing book cover images if available, and exporting the list of new acquisitions in various file formats.
I knocked out the book cover images request quickly. It took me a little longer
to replicate the style of the new website, especially since the PHP templates
used <table>
elements for positioning, but the new website used more modern
(but still pre-Flexbox and Grid) CSS techniques, so I had to rework the styles
myself. But I was pleased with the result, and the library staff were happy too.
And, of course, I was better than my predecessor: I was security-conscious, and I knew Linux from the bad ol' days of manually compiling drivers for every single device on your machine. The app had languished, running on an outdated version of PHP in an outdated version of Apache on an unsupported OS version. I wasn't able to update the server OS at that time, so I compiled current versions of Apache and PHP that could run on the server to get the latest security patches. This version now included PHP prepare statements, and I converted all the SQL statements to use it. And I removed unnecessary permissions from the user. I was feeling good, safe, and smug. But with all that work that I did, I missed that I had introduced a vulnerability that was obvious in hindsight.
Now Introducing: Arbitrary Read Vulnerability
The library staff wanted a feature to export the list of new books to print out announcement pages to post in the library, which saved them a lot of time compiling and editing the list manually each month. The export functionality was available to all users, since some patrons may have been interested in downloading the list to look at offline. (Sidenote: they weren't).
While implementing the export feature, I borrowed some code from another
application that did a similar thing. However, I had to remove a dependency
because it was no longer compatible with the new version of PHP, which required
me to reimplement the file generation and download endpoints. You clicked the
button, which sent a POST request to an export.php
script, which ran the same
SQL query that generated the website, formatted the results in CSV, TXT or
DOCX, saved the file to a temporary file location, and finally,
redirected the user to download the same file. Those of you wiser than me can
probably guess which vulnerability I introduced: path traversal.
When I first learned about path traversal, I pronounced it "path transversal," which my boss kindly corrected. Thanks, P.
I thought through this from the perspective of the user; to them, it seemed
atomic: click the button, get a file. It seemed secure to me. But it's
important to think like an attacker in order to secure your applications
against them. And obviously it wasn't atomic, since I had to write two
different PHP files: export.php
and download.php
. I don't remember all the
details, but the code was something like this:
<!-- export.php -->
<?php
$format = $_POST["format"];
$startRange = $_POST["start"];
$endRange = $_POST["end"];
$results = getNewAcquisitions($startRange, $endRange);
$exportFile = tempnam($GLOBALS["exportDir"], "newacq-");
switch ($format) {
case "csv":
// write to file as CSV
break;
case "txt":
// write to file as TXT
break;
case "docx";
// write to file as DOCX
break;
}
$filename = pathinfo($exportFile, PATHINFO_BASENAME)
header("Location: download.php?file=${filename}&format=${format}", true, 302);
?>
<!-- download.php -->
<?php
$filename = $_GET["file"];
$format = $_GET["format"];
$contentType = "text/plain";
if ($format == 'csv') {
$contentType = "text/csv";
} else if ($format == 'docx') {
$contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
header("Content-Type: $contentType");
$path = $GLOBALS["exportDir"] . DIRECTORY_SEPARATOR . $filename;
readfile($path);
?>
The export endpoint generated the file and saved it, and the download endpoint
took the filename of the newly generated file and concatenated it with the path
to the export directory on the server. However, I failed to sanitize the input:
I assumed that the input was a filename, but used it as if it were a relative
path with a single component. The difference is that an attacker could add any
number of components to have the PHP, including ../
, which allowed them to
traverse to any directory on the server.
So then instead of delivering /var/www/newacq/export/randomid.csv
, the
attacker could ask the server to deliver
/var/www/newacq/export/randomid.csv/../../../../etc/passwd
, for example. With
this vulnerability, attackers could read any file that the web server user had
access to, including database config files and other credentials.
Lessons Learned
I didn't find this out on my own; our IT department conducted our yearly penetration test with some security contractors. After they pointed it out, I slapped my forehead and constrained the filename parameter for the download input to exactly match the format generated by the export endpoint, and rotated the database credentials. After the fix was in place and the pentesters confirmed the remediation, I began digging into common vulnerabilities from OWASP beyond the top 10 (though this was on the list at the time) to develop a better sense of the attack vectors I should be watching out for.
I learned a few things from this incident:
- Think like an attacker, not just a user or admin, when evaluating security.
- Don't assume that your input only does what you think it does; analyze how you're using it, and if the set of inputs is larger than what you need it to be, make sure you constrain it to prevent unknown future bugs.
- It's always good to conduct security reviews, whether by your internal team members or external teams.
- Always be humble. Everyone can make mistakes, and we're all learning.