All Docs
FeaturesCalmony PayUpdated March 15, 2026

Security Advisory: SEC-30 — File Upload Content Type Validation

Security Advisory: SEC-30 — File Upload Content Type Validation

Severity: High Component: Upload Router (src/lib/routers/upload.ts) Version Identified: 1.0.59 Control ID: SEC-30 Category: API Security


Overview

The Calmony Pay upload router validates permitted file types by checking a client-supplied contentType value against a server-side allowlist (ALLOWED_IMAGE_TYPES). Because this value originates from the client, it cannot be trusted as proof of actual file content. A malicious client can declare any permitted MIME type while uploading a file of a completely different kind.

This advisory documents the vulnerability, explains the attack surface, and details the recommended mitigations.


Vulnerability Detail

How the Upload Flow Works

  1. The client calls the upload endpoint, providing a contentType string (e.g. image/png).
  2. The server checks contentType against ALLOWED_IMAGE_TYPES.
  3. If allowed, the server issues a presigned S3 PUT URL.
  4. The client uploads the file directly to S3 using the presigned URL. S3 enforces that the Content-Type header on the PUT matches what was declared — but it does not inspect file content.

The Trust Problem

Step 2 trusts a value provided by the client. Any HTTP client can set contentType to image/png and then upload an entirely different file. S3's enforcement at step 4 only checks that the header matches the string agreed at presign time — both values were supplied by the same attacker.

SVG as an Elevated Risk

image/svg+xml is included in the current ALLOWED_IMAGE_TYPES allowlist. SVG is an XML-based format that natively supports embedded JavaScript via <script> tags and event handler attributes. When such a file is served directly by S3 (or a CDN) with Content-Type: image/svg+xml, browsers will render and execute any embedded scripts — making this a viable stored cross-site scripting (XSS) vector.

Polyglot and Disguised Files

Beyond SVG, an attacker can upload polyglot files — for example, a file that is simultaneously a valid PNG and a valid ZIP archive, or a file whose declared type differs from its actual format. Without magic-byte inspection, the server has no way to detect this.


Attack Scenarios

ScenarioDeclared TypeActual ContentRisk
SVG with embedded JSimage/svg+xmlSVG + <script>Stored XSS when served to users
PNG-declared SVGimage/pngSVG with scriptsBypasses SVG-specific restrictions
Polyglot fileimage/jpegZIP / HTML hybridArbitrary content stored and served

Recommended Mitigations

1. Remove or Restrict SVG from the Allowlist

The simplest immediate mitigation is to remove image/svg+xml from ALLOWED_IMAGE_TYPES in src/lib/routers/upload.ts.

If SVG uploads are a product requirement, ensure the S3 bucket and CDN override the response Content-Type to text/plain and add Content-Disposition: attachment for all SVG objects. This prevents browsers from rendering SVG inline and executing embedded scripts.

# Example CloudFront / S3 response headers for SVG objects
Content-Type: text/plain
Content-Disposition: attachment; filename="file.svg"

2. Server-Side Magic-Byte Validation

After a file is uploaded to S3, verify that the first bytes of the stored object match the expected magic bytes for the declared MIME type before making the file available.

This can be implemented as:

  • An S3 Event Notification → Lambda trigger that reads the object head bytes, validates the signature, and deletes or quarantines the object if it fails.
  • A post-upload verification step within the presign flow, if the architecture allows a synchronous read-after-write.

Example magic bytes for common image types:

FormatMagic Bytes (hex)
JPEGFF D8 FF
PNG89 50 4E 47 0D 0A 1A 0A
GIF47 49 46 38
WebP52 49 46 46 … 57 45 42 50

SVG has no magic bytes (it is plain text XML), which further reinforces removing it from the allowlist or handling it as a distinct, restricted upload type.

3. Content Security Policy at the CDN Layer

Ensure the CloudFront distribution serving user-uploaded content includes a strict Content-Security-Policy response header:

Content-Security-Policy: default-src 'none'; script-src 'none';

This provides defence-in-depth: even if a malicious file is stored and served, the browser will refuse to execute any scripts it contains.


Affected File

src/lib/routers/upload.ts

No Breaking API Changes

This advisory does not introduce any changes to the public API contract. All recommended mitigations are internal to the upload infrastructure and transparent to well-behaved clients.


References