Bypassing File Upload Restrictions To Exploit Client-Side Path Traversal

# Bypassing File Upload Restrictions To Exploit Client-Side Path Traversal

07 Jan 2025 – Posted by Maxence Schmitt

In my previous blog post, I demonstrated how a JSON file could be used as a gadget for Client-Side Path Traversal (CSPT) to perform Cross-Site Request Forgery (CSRF). That example was straightforward because no file upload restriction was enforced. However, real-world applications often impose restrictions on file uploads to ensure security.

In this post, we’ll explore how to bypass some of these mechanisms to achieve the same goal. We’ll cover common file validation methods and how they can be subverted.

# Constraint

In most scenarios, the gadget file will be parsed in the front-end using JSON.parse. It means that our file must be a valid input for JSON.parse. If we look at the V8 implementation. A valid JSON input is :

– a string
– a number
– true
– false
– null
– an array
– an object

The parser skips starting WHITESPACE characters such as :

– ’ ‘
– ‘\t’
– ‘\r’
– ‘\n’

Also, control characters and double quotes inside a JSON object (key or value) will break the JSON structure and must be escaped.

Our gadget file must follow these restrictions to be parsed as JSON.

Different applications validate files using libraries or tools designed to detect the file’s MIME type, file structure or magic bytes. By creatively crafting files that meet these conditions, we can fool these validations and bypass the restrictions.

Let’s explore how various file upload mechanisms can be bypassed to maintain valid JSON payloads for CSPT while satisfying file format requirements, such as PDFs or images.

# Bypassing PDF Checks To Upload a JSON File

A basic check in many upload mechanisms involves verifying the file’s MIME type. This is often done using the `Content-Type` header or by inspecting the file itself. However, these checks can often be bypassed by manipulating the file’s structure or headers.

## Bypassing mmmagic Validation

The mmmagic library is commonly used in Node.js applications to detect file types based on the Magic database. A PDF file can be verified with the following code:

“`
async function checkMMMagic(binaryFile) { var magic = new Magic(mmm.MAGIC_MIME_TYPE); const detectAsync = (binaryFile) => { return new Promise((resolve, reject) => { magic.detect.call(magic, binaryFile, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); }; const result = await detectAsync(binaryFile); const isValid = (result === ‘application/pdf’) if (!isValid) { throw new Error(‘mmmagic: File is not a PDF : ‘ + result); } }
“`

### Technique:

The library checks for the `%PDF` magic bytes. It uses the Magic detection rules defined here. However, according to the PDF specification, this magic number doesn’t need to be at the very beginning of the file.

We can wrap a PDF header within the first 1024 bytes of a JSON object. It will be a valid JSON file considered as a PDF by the library. This allows us to fool the library into accepting the upload as a valid PDF while still allowing it to be parsed as JSON by the browser. Here’s an example:

`{ “id” : “../CSPT_PAYLOAD”, “%PDF”: “1.4” }`

As long as the `%PDF` header appears within the first 1024 bytes, the `mmmagic` library will accept this file as a PDF, but it can still be parsed as JSON on the client side.

## Bypassing pdflib Validation

The pdflib library requires more than just the `%PDF` header. It can be used to validate the overall PDF structure.

“`
async function checkPdfLib(binaryFile) { let pdfDoc = null try { pdfDoc = await PDFDocument.load(binaryFile); } catch (error) { throw new Error(‘pdflib: Not a valid PDF’) } if (pdfDoc.getPageCount() == 0) { throw new Error(‘pdflib: PDF doesn’t have a page’); } }
“`

### Technique:

To bypass this, we can create a valid PDF (for pdflib) that still conforms to the JSON structure required for CSPT.
The trick is to replace `%0A` (line feed) characters between PDF object definitions with space `%20`. This allows the file to pass as a valid PDF for `pdflib` but still be interpretable as JSON. The xref table doesn’t need to be fixed because our goal is not to display the PDF, but to pass the upload validation.

Here’s an example:

“`
{“_id”:”../../../../CSPT?”,”bypass”:”%PDF-1.3 1 0 obj > endobj 2 0 obj > endobj 3 0 obj > >> /Type /Page >> endobj 4 0 obj > stream BT /F1 10 Tf 20 100 Td (CSPT) Tj ET endstream endobj 5 0 obj > endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000062 00000 n 0000000133 00000 n 0000000277 00000 n 0000000370 00000 n trailer > startxref 447 %%EOF “}
“`

While this PDF will not render in recent PDF viewers, it will be readable by `pdflib` and pass the file upload checks.

## Bypassing file Command Validation

In some environments, the `file` command or a library based on `file` is used to detect file types.

“`
async function checkFileCommand(binaryFile) { //Write a temporary file const tmpobj = tmp.fileSync(); fs.writeSync(tmpobj.fd, binaryFile); fs.closeSync(tmpobj.fd); // Exec file command output = execFileSync(‘file’, [“-b”, “–mime-type”, tmpobj.name]) const isValid = (output.toString() === ‘application/pdfn’) if (!isValid) { throw new Error(`content – type: File is not a PDF : ${output}`); } tmpobj.removeCallback(); }
“`

### Technique:

The difference with `mmmagic` is that before checking the magic bytes, it tries to parse the file as JSON. If it succeed, the file is considerered to be JSON and no other checks will be perform. So we can’t use the same trick as mmmagic. However, the `file` command has a known limit on the size of files it can process. This is an extract of the `man file` command.

“`
-P, –parameter name=value Set various parameter limits. Name Default Explanation bytes 1048576 max number of bytes to read from file elf_notes 256 max ELF notes processed elf_phnum 2048 max ELF program sections processed elf_shnum 32768 max ELF sections processed encoding 65536 max number of bytes to scan for encoding evaluation indir 50 recursion limit for indirect magic name 60 use count limit for name/use magic regex 8192 length limit for regex searches
“`

We can see a limit on the number of bytes to read. We can exploit this limit by padding the file with whitespace characters (such as spaces or tabs) until the file exceeds the parsing limit. Once the limit is reached, the `file_is_json` function will fail, and the file will be classified as a different file type (e.g., a PDF).

For example, we can create a file like this:

“`
{ “_id”: “../../../../CSPT?”, “bypass”: “%PDF-1.3 1 0 obj > endobj 2 0 obj > endobj 3 0 obj > >> /Type /Page >> endobj 4 0 obj > stream BT /F1 10 Tf 20 100 Td (CSPT) Tj ET endstream endobj 5 0 obj > endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000062 00000 n 0000000133 00000 n 0000000277 00000 n 0000000370 00000 n trailer > startxref 447 %%EOF ” }
“`

When uploaded, the file command will be unable to parse this large JSON structure, causing it to fall back to normal file detection and to treat the file as a PDF.

# Bypassing Image Upload file-type Restriction Using the WEBP Format

Image uploads often use libraries like `file-type` to validate file formats. The following code tries ensure that the uploaded file is an image.

“`
const checkFileType = async (binary) => { const { fileTypeFromBuffer } = await fileType(); const type = await fileTypeFromBuffer(binary); const result = type.mime; const isValid = result.startsWith(‘image/’); if (!isValid) { throw new Error(‘file-type: File is not an image : ‘ + result); } };
“`

## Technique:

Sometimes, these libraries check for specific magic numbers at a predefined offset. In this example, file-type checks if the magic bytes are present at offset 8 : https://github.com/sindresorhus/file-type/blob/v19.6.0/core.js#L358C1-L363C1

“`
if (this.checkString(‘WEBP’, {offset: 8})) { return { ext: ‘webp’, mime: ‘image/webp’, }; }
“`

As we have control over the starting bytes, we can build a valid JSON file. We can craft a JSON object that places the magic bytes ( `WEBP`) at the correct offset, allowing the file to pass validation as an image while still being a valid JSON object. Here’s an example:

“`
{“aaa”:”WEBP”,”_id”:”../../../../CSPT?”}
“`

This file will pass the `file-type` check for images, while still containing JSON data that can be used for CSPT.

# In Conclusion

Of course, bypassing file-upload restrictions is not new but we wanted to share some methods we used in past years to upload JSON gadgets when file-upload restrictions are implemented. We used them in order to perform CSPT2CSRF or any other exploits (XSS, etc.) but they can be applied in other contexts. Don’t hesitate to dig into third-party source code in order to understand how it works.

All these examples and files have been included in the CSPTPlayground. The playground doesn’t only include CSPT2CSRF but also other examples such as a JSONP gadget or Open Redirect to perform other exploits. It was built based on feedback about CSPT given by Isira Adithya(@isira_adithya) and Justin Gardner (@Rhynorater).

# More information

If you would like to learn more about our other research, check out our blog, follow us on X (@doyensec) or feel free to contact us at [email protected] for more information on how we can help your organization “Build with Security”.

Leave a Reply

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