Press "Enter" to skip to content

PHP File Upload Source Analysis (RFC1867)

File uploads generally come in two flavors: FTP and HTTP. For our internet applications: FTP upload is stable in transmission, but both usability and security are problems. You surely wouldn't tell a user who wants to upload an avatar "please open your FTP client, upload the file to http://www.laruence.com/uploads/, and name it 2dk433423l.jpg", right?
HTTP-based upload, by comparison, is much better on both usability and security. The applicable upload methods are PUT, WEBDAV, and RFC1867. This article analyzes how PHP implements file upload based on RFC1867.

RFC1867

RFC1867 is the Form-based File Upload in HTML standard protocol. The RFC1867 standard makes two changes to HTML:

1 Adds a "file" option to the type attribute of the input element.
2 The input tag may have an accept attribute, which specifies the list of file types or formats that can be uploaded.

In addition, the standard defines a new MIME type: multipart/form-data, as well as the behavior to be adopted when handling a form that has enctype="multipart/form-data" and/or contains an <input type="file"> tag.

For example, when HTML wants to let users upload one or more files, it can be written as:

<form enctype="multipart/form-data" action="upload.php" method=post>
Select file:
<input name="userfile" type="file">
File description:
<input name="description" type="text">
<input type="submit" value="Upload">
</form>

This form should look familiar to everyone. As for PHP, it defines its own extra default form element MAX_FILE_SIZE, through which users can suggest to PHP the maximum allowed upload file size. For example, in the case above, if we want the uploaded file to be no larger than 5000 (5k) bytes, we can write:

<form enctype="multipart/form-data" action="upload.php" method=post>
<input type="hidden" value="5000" name="MAX_FILE_SIZE"> <!-- file size -->
Select file:
<input name="userfile" type="file">
File description:
<input name="description" type="text">
<input type="submit" value="Upload">
</form>

Setting aside just how unreliable this MAX_FILE_SIZE is (and therefore how unreliable all browser-side controls are), let's purely look at the implementation of how MAX_FILE_SIZE takes effect.
When the user selects a file (laruence.txt), fills in the file description ("laruence's personal profile"), and clicks upload, what happens?

Form submission

After the user confirms the submission, the browser reads the file to be uploaded based on the user's selection, organizes it together with the other form elements into data of a certain format (as below), and sends it to the page specified by the form's action attribute (upload.php in this example):

// request header
POST /upload.php HTTP/1.0\r\n
...
Host: www.laruence.com\r\n
...
Content-length: xxxxx\r\n
...
Content-type: multipart/form-data, boundary=7d51863950254\r\n
...\r\n\r\n
// start of POST data content
--7d51863950254
content-disposition: form-data; name="description"\r\n
laruence's personal profile
--7d51863950254
content-disposition: form-data; name="userfile"; filename="laruence.txt"
Content-Type: text/plain\r\n
... contents of laruence.txt ...
--7d51863950254--

Next, it's about how the server handles this data.

Receiving the upload

When the web server — assumed here to be Apache (and further assuming PHP is installed on Apache as a module) — receives the user's data, it first determines from the HTTP request headers that the MIME TYPE is a PHP type, then after some processing (for this part, see my earlier PHP Life Cycle ppt), it finally hands control over to the PHP module.
At this point PHP calls sapi_activate to initialize a request. During this process it first determines the request type — here it is POST — and so calls sapi_read_post_data, which finds the rfc1867 handler function rfc1867_post_handler based on the Content-type, then invokes that handler to parse the POSTed data.
The source code for rfc1867_post_handler can be found in main/rfc1867.c; you can also refer to my earlier in-depth look at PHP file upload, which also lists the source code.
Then, using the boundary, PHP checks each segment to see whether it defines both:

	name and filename attributes (named file upload)
	filename defined but no name (unnamed upload)
	name defined but no filename (regular data)

and handles each differently.

if ((cd = php_mime_get_hdr_value(header, "Content-Disposition"))) {
	char *pair=NULL;
	int end=0;
	while (isspace(*cd)) {
		++cd;
	}
	while (*cd && (pair = php_ap_getword(&cd, ';')))
	{
		char *key=NULL, *word = pair;
		while (isspace(*cd)) {
			++cd;
		}
		if (strchr(pair, '=')) {
			key = php_ap_getword(&pair, '=');
			if (!strcasecmp(key, "name")) {
				// get the name field
				if (param) {
					efree(param);
				}
				param = php_ap_getword_conf(&pair TSRMLS_CC);
			} else if (!strcasecmp(key, "filename")) {
				// get the filename field
				if (filename) {
					efree(filename);
				}
				filename = php_ap_getword_conf(&pair TSRMLS_CC);
			}
		}
		if (key) {
			efree(key);
		}
		efree(word);
	}

During this process PHP also checks whether the regular data contains a MAX_FILE_SIZE.

 /* Normal form variable, safe to read all data into memory */
if (!filename && param) {
	unsigned int value_len;
	char *value = multipart_buffer_read_body(mbuff, &value_len TSRMLS_CC);
	unsigned int new_val_len; /* Dummy variable */
	......
	if (!strcasecmp(param, "MAX_FILE_SIZE")) {
                  max_file_size = atol(value);
    }
	efree(param);
	efree(value);
	continue;
}

If present, its value is used to check whether the file size is exceeded.

if (PG(upload_max_filesize) > 0 && total_bytes > PG(upload_max_filesize)) {
	cancel_upload = UPLOAD_ERROR_A;
} else if (max_file_size && (total_bytes > max_file_size)) {
#if DEBUG_FILE_UPLOAD
	sapi_module.sapi_error(E_NOTICE,
		"MAX_FILE_SIZE of %ld bytes exceeded - file [%s=%s] not saved",
		 max_file_size, param, filename);
#endif
	cancel_upload = UPLOAD_ERROR_B;
}

From the code above we can also see that the check has two parts. The first part checks PHP's default upload limit. Only the second part checks the user-defined MAX_FILE_SIZE — so a MAX_FILE_SIZE defined in the form cannot exceed the maximum upload file size configured in PHP.
Based on the name and filename determination, if it's a file upload, a temporary file with a random name is created in the upload directory according to PHP's settings:

 if (!skip_upload) {
	/* Handle file */
	fd = php_open_temporary_fd_ex(PG(upload_tmp_dir),
			 "php", &temp_filename, 1 TSRMLS_CC);
	if (fd==-1) {
		sapi_module.sapi_error(E_WARNING,
			 "File upload error - unable to create a temporary file");
		cancel_upload = UPLOAD_ERROR_E;
	}
}

It returns the file handle and the temporary random filename.
Afterwards there are further validations, such as whether the filename is valid and the name is valid.
If all these validations pass, the content is read in and written to this temporary file.

.....
else if (blen > 0) {
	wlen = write(fd, buff, blen); // write to the temporary file
	if (wlen == -1) {
	/* write failed */
#if DEBUG_FILE_UPLOAD
	sapi_module.sapi_error(E_NOTICE, "write() failed - %s", strerror(errno));
#endif
	cancel_upload = UPLOAD_ERROR_F;
	}
}
....

When the read loop completes, the temporary file handle is closed and the temporary variable name is recorded:

zend_hash_add(SG(rfc1867_uploaded_files), temp_filename,
	strlen(temp_filename) + 1, &temp_filename, sizeof(char *), NULL);

And the FILE variable is generated. At this point, if it's a named upload, the following is set:

$_FILES['userfile'] // name="userfile"

If it's an unnamed upload, tmp_name is used to set it:

$_FILES['tmp_name'] // unnamed upload

Finally it is handed to the user-written upload.php for processing.
At this point in upload.php, the user can operate on the file just generated via move_uploaded_file.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.