f5278f0 security: harden file serving endpoints against stored XSS
c0f2d50 security: address review feedback on file serving XSS hardening
📊 Changes
1 file changed (+62 additions, -14 deletions)
View changed files
📝backend/open_webui/routers/files.py (+62 -14)
📄 Description
Summary
Prevent stored XSS attacks through file-serving endpoints by validating content types against a safe allowlist and adding security headers.
Problem
The file-serving endpoints in backend/open_webui/routers/files.py pass through the uploader-controlled Content-Type header directly to the response. An attacker can:
GET /{id}/content — Upload a file with Content-Type: text/html containing malicious <script> tags. When any authenticated user with access visits the file URL, the browser renders it as HTML under the app's origin, enabling full same-origin script execution (cookie theft, API calls as victim, data exfiltration).
GET /{id}/content/html — This endpoint serves files with their OS-inferred content type and no Content-Security-Policy. An admin-uploaded .html file executes JavaScript freely when visited by any authenticated user.
Both vectors result in stored XSS — the payload persists in the uploaded file and triggers on every visit.
Fix
Content-type allowlist (default-deny)
Added SAFE_INLINE_CONTENT_TYPES — a set of content types known to be safe for inline rendering (images, PDF, plain text, audio, video). Files with content types not on the allowlist are forced to Content-Disposition: attachment with media_type=application/octet-stream, preventing browser execution.
Notable exclusion: image/svg+xml is deliberately not in the allowlist because SVGs served as direct document responses can execute JavaScript via <script> tags and onload handlers. SVGs loaded via <img> tags (the normal frontend path) are safe since browsers disable scripting in that context.
MIME parameters (e.g. image/png; charset=utf-8) are stripped before comparison so legitimate files are not inadvertently blocked.
CSP sandbox on /content/html
The HTML preview endpoint now includes Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data: which blocks script execution while still allowing the HTML to render for preview purposes.
X-Content-Type-Options: nosniff
Added to all three file-serving endpoints (/{id}/content, /{id}/content/html, /{id}/content/{file_name}) to prevent browsers from MIME-sniffing uploaded files as executable content.
Behavioral impact
File type
Before
After
PDF, images, text/plain, audio, video
Inline
Inline (unchanged)
text/html, application/javascript, etc.
Inline with attacker-controlled type
Forced download as octet-stream
SVG
Inline (XSS risk)
Forced download
HTML preview (/content/html)
Unrestricted execution
CSP sandboxed (no scripts)
No changes to upload logic, storage, or access control. Only the response headers and content type on file-serving responses are affected.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.
## 📋 Pull Request Information
**Original PR:** https://github.com/open-webui/open-webui/pull/24372
**Author:** [@SebTardif](https://github.com/SebTardif)
**Created:** 5/5/2026
**Status:** ❌ Closed
**Base:** `main` ← **Head:** `fix/file-serving-xss-hardening`
---
### 📝 Commits (2)
- [`f5278f0`](https://github.com/open-webui/open-webui/commit/f5278f0baa4402053efcdfa1d5dd7826869fe156) security: harden file serving endpoints against stored XSS
- [`c0f2d50`](https://github.com/open-webui/open-webui/commit/c0f2d503bf36ec67a7943eece17bd13aae2dca72) security: address review feedback on file serving XSS hardening
### 📊 Changes
**1 file changed** (+62 additions, -14 deletions)
<details>
<summary>View changed files</summary>
📝 `backend/open_webui/routers/files.py` (+62 -14)
</details>
### 📄 Description
## Summary
Prevent stored XSS attacks through file-serving endpoints by validating content types against a safe allowlist and adding security headers.
## Problem
The file-serving endpoints in `backend/open_webui/routers/files.py` pass through the uploader-controlled `Content-Type` header directly to the response. An attacker can:
1. **`GET /{id}/content`** — Upload a file with `Content-Type: text/html` containing malicious `<script>` tags. When any authenticated user with access visits the file URL, the browser renders it as HTML under the app's origin, enabling full same-origin script execution (cookie theft, API calls as victim, data exfiltration).
2. **`GET /{id}/content/html`** — This endpoint serves files with their OS-inferred content type and no Content-Security-Policy. An admin-uploaded `.html` file executes JavaScript freely when visited by any authenticated user.
Both vectors result in **stored XSS** — the payload persists in the uploaded file and triggers on every visit.
## Fix
### Content-type allowlist (default-deny)
Added `SAFE_INLINE_CONTENT_TYPES` — a set of content types known to be safe for inline rendering (images, PDF, plain text, audio, video). Files with content types not on the allowlist are forced to `Content-Disposition: attachment` with `media_type=application/octet-stream`, preventing browser execution.
Notable exclusion: `image/svg+xml` is deliberately **not** in the allowlist because SVGs served as direct document responses can execute JavaScript via `<script>` tags and `onload` handlers. SVGs loaded via `<img>` tags (the normal frontend path) are safe since browsers disable scripting in that context.
MIME parameters (e.g. `image/png; charset=utf-8`) are stripped before comparison so legitimate files are not inadvertently blocked.
### CSP sandbox on `/content/html`
The HTML preview endpoint now includes `Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:` which blocks script execution while still allowing the HTML to render for preview purposes.
### `X-Content-Type-Options: nosniff`
Added to all three file-serving endpoints (`/{id}/content`, `/{id}/content/html`, `/{id}/content/{file_name}`) to prevent browsers from MIME-sniffing uploaded files as executable content.
## Behavioral impact
| File type | Before | After |
|---|---|---|
| PDF, images, text/plain, audio, video | Inline | Inline (unchanged) |
| text/html, application/javascript, etc. | Inline with attacker-controlled type | Forced download as octet-stream |
| SVG | Inline (XSS risk) | Forced download |
| HTML preview (`/content/html`) | Unrestricted execution | CSP sandboxed (no scripts) |
No changes to upload logic, storage, or access control. Only the response headers and content type on file-serving responses are affected.
---
<sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
📋 Pull Request Information
Original PR: https://github.com/open-webui/open-webui/pull/24372
Author: @SebTardif
Created: 5/5/2026
Status: ❌ Closed
Base:
main← Head:fix/file-serving-xss-hardening📝 Commits (2)
f5278f0security: harden file serving endpoints against stored XSSc0f2d50security: address review feedback on file serving XSS hardening📊 Changes
1 file changed (+62 additions, -14 deletions)
View changed files
📝
backend/open_webui/routers/files.py(+62 -14)📄 Description
Summary
Prevent stored XSS attacks through file-serving endpoints by validating content types against a safe allowlist and adding security headers.
Problem
The file-serving endpoints in
backend/open_webui/routers/files.pypass through the uploader-controlledContent-Typeheader directly to the response. An attacker can:GET /{id}/content— Upload a file withContent-Type: text/htmlcontaining malicious<script>tags. When any authenticated user with access visits the file URL, the browser renders it as HTML under the app's origin, enabling full same-origin script execution (cookie theft, API calls as victim, data exfiltration).GET /{id}/content/html— This endpoint serves files with their OS-inferred content type and no Content-Security-Policy. An admin-uploaded.htmlfile executes JavaScript freely when visited by any authenticated user.Both vectors result in stored XSS — the payload persists in the uploaded file and triggers on every visit.
Fix
Content-type allowlist (default-deny)
Added
SAFE_INLINE_CONTENT_TYPES— a set of content types known to be safe for inline rendering (images, PDF, plain text, audio, video). Files with content types not on the allowlist are forced toContent-Disposition: attachmentwithmedia_type=application/octet-stream, preventing browser execution.Notable exclusion:
image/svg+xmlis deliberately not in the allowlist because SVGs served as direct document responses can execute JavaScript via<script>tags andonloadhandlers. SVGs loaded via<img>tags (the normal frontend path) are safe since browsers disable scripting in that context.MIME parameters (e.g.
image/png; charset=utf-8) are stripped before comparison so legitimate files are not inadvertently blocked.CSP sandbox on
/content/htmlThe HTML preview endpoint now includes
Content-Security-Policy: sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:which blocks script execution while still allowing the HTML to render for preview purposes.X-Content-Type-Options: nosniffAdded to all three file-serving endpoints (
/{id}/content,/{id}/content/html,/{id}/content/{file_name}) to prevent browsers from MIME-sniffing uploaded files as executable content.Behavioral impact
/content/html)No changes to upload logic, storage, or access control. Only the response headers and content type on file-serving responses are affected.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.