
# Upload Security

File upload is the classic soft spot of a data application: it accepts attacker-chosen bytes, an attacker-chosen name, and an attacker-chosen content type, then serves all three back to a browser. Sovrium's answer is that every one of those three is validated server-side, in a fixed order, before anything is written.

## Before Anything Is Stored

Validation runs in this sequence. Each step is total — nothing "sanitizes and continues".

| Step | Check                                                                | Rejection |
| ---- | -------------------------------------------------------------------- | --------- |
| 1    | Filename contains `..`, `/`, `\` — path traversal.                   | `400`     |
| 2    | Filename contains a null byte.                                       | `400`     |
| 3    | Size exceeds the bucket's `maxFileSize`, or `STORAGE_MAX_FILE_SIZE`. | `413`     |
| 4    | MIME type is not in the bucket's `allowedMimeTypes`.                 | `400`     |
| 5    | Bucket is private and the request has no session.                    | `401`     |
| 6    | Storing the file would exceed `STORAGE_MAX_TOTAL_SIZE`.              | `507`     |

Steps 1 to 4 run **before** the auth check. That ordering is intentional: an oversized or malformed upload is rejected on its own merits, so an unauthenticated client cannot use the auth boundary to learn whether its payload would otherwise have been accepted.

An explicit `path` field follows a slightly different rule set — `/` is allowed, since path prefixes are the whole point, but a leading `/`, an empty value, `..`, `\` and null bytes are all rejected.

## MIME Allow-Lists

`allowedMimeTypes` is the bucket's statement about what belongs in it. Entries match exactly, or as a `type/*` prefix wildcard:

```yaml
buckets:
  - name: avatars
    allowedMimeTypes: [image/*] # image/png ✓  image/svg+xml ✓  application/pdf ✗

  - name: invoices
    allowedMimeTypes: [application/pdf] # exact match only
```

Omitting the property accepts everything. That is fine for an internal bucket and reckless for one that any authenticated user can write to — an allow-list is the cheapest control on this page, and the only one that expresses intent rather than merely blocking an attack.

:::callout
**The MIME type is the client's claim, not a fact.** It comes from the browser's multipart part header, and nothing re-derives it from the bytes. A `.pdf` allow-list stops an accidental upload and a lazy attacker; it does not stop a deliberate one from labelling arbitrary bytes `application/pdf`. Pair the allow-list with the response headers below, which are what actually neutralise a mislabelled file.
:::

## What a Served File Carries

Downloads — direct or through a [signed URL](/en/docs/signed-urls) — are served with three headers whose combined job is to make a stored file inert in a browser:

| Header                    | Value                                  | Why                                                                 |
| ------------------------- | -------------------------------------- | ------------------------------------------------------------------- |
| `X-Content-Type-Options`  | `nosniff`                              | Stops the browser second-guessing the type and executing the bytes. |
| `Content-Security-Policy` | `default-src 'none'`                   | Even if rendered, no script, style or network access is permitted.  |
| `Content-Disposition`     | `attachment` (or `inline` — see below) | Downloads rather than renders, in a context where that matters.     |

`Content-Disposition` is the one with nuance. Only **raster** images — PNG, JPEG, GIF, WebP — are ever served `inline`, and then only through the signed-download route where inline display is the point. Everything else is forced to `attachment`, and `image/svg+xml` is explicitly excluded from the inline set: an SVG is a document that can carry `<script>`, so serving one inline on your own origin is stored XSS. Non-ASCII filenames are emitted with RFC 5987 `filename*=UTF-8''…` so a header never breaks on a multi-byte name.

## What This Does Not Cover

Two things the platform will not do for you:

- **No virus scanning.** Bytes are stored as received. If your threat model includes malware distribution, put a scanner in front of the upload endpoint or behind an [automation](/en/docs/automations-overview).
- **No content-based type detection.** See the callout above — declared types are trusted for the allow-list decision.

## Related Pages

- [File Operations](/en/docs/file-operations) — the endpoints these rules guard.
- [Bucket Permissions](/en/docs/buckets-permissions) — who may reach them in the first place.
- [Lifecycle & Quotas](/en/docs/file-lifecycle) — the size caps referenced above.
- [Signed URLs](/en/docs/signed-urls) — access without a session, and its own headers.
- [Buckets Overview](/en/docs/buckets-overview) — where `allowedMimeTypes` is declared.
