fd83612 security: harden file serving endpoints against stored XSS
addc526 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
Pull Request Checklist
Before submitting, make sure you've checked the following:
Target branch: Verify that the pull request targets the dev branch.
Description: Provided below.
Changelog: Included below.
Documentation: No user-facing config changes; no docs needed.
Dependencies: No new dependencies.
Testing: Manually verified that PDFs, images, and text/plain files still serve inline. HTML and SVG files are now forced to download. The /content/html endpoint still renders HTML but with CSP sandbox blocking script execution.
Agentic AI Code: This PR has gone through human review and manual testing.
Code review: Self-reviewed; also ran an agent code review that caught SVG in the allowlist (fixed) and dead code (cleaned up).
Design & Architecture: No new settings or UI changes.
Git Hygiene: Atomic PR, rebased on dev, two clean commits.
Title Prefix:fix:
Changelog Entry
Description
Harden file-serving endpoints against stored XSS by validating content types against a safe allowlist and adding security headers. Previously, an attacker could upload a file with Content-Type: text/html containing malicious scripts, and any authenticated user viewing the file would execute the scripts under the app's origin.
Added
SAFE_INLINE_CONTENT_TYPES allowlist for content types safe to serve inline (images, PDF, plain text, audio, video)
_safe_content_type() helper that validates and normalizes content types against the allowlist
Changed
GET /{id}/content now validates content type against allowlist; unsafe types forced to application/octet-stream + attachment download
GET /{id}/content/html now includes Content-Security-Policy: sandbox header blocking script execution
GET /{id}/content/{file_name} now explicitly sets media_type=application/octet-stream
All three endpoints now include X-Content-Type-Options: nosniff
MIME parameters stripped before allowlist comparison to prevent bypasses
Breaking Changes
SVG files served via /{id}/content will now download instead of displaying inline. SVGs loaded via <img> tags in the frontend are unaffected since browsers disable scripting in that context.
Files with exotic content types (e.g. text/html, application/javascript) will now force-download instead of rendering inline.
Additional Information
The /content/html endpoint is already restricted to files owned by admin-role users (line 724 check), but in multi-admin deployments a compromised admin could target other admins. The CSP sandbox provides defense-in-depth.
The users.py and channels.py profile image endpoints also decode data URIs with user-controlled media types — these are out of scope for this PR but could be addressed separately.
Contributor License Agreement
By submitting this pull request, I confirm that I have read and fully agree to the Contributor License Agreement (CLA), and I am providing my contributions under its terms.
Note
Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
🔄 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/24373
**Author:** [@SebTardif](https://github.com/SebTardif)
**Created:** 5/5/2026
**Status:** ❌ Closed
**Base:** `dev` ← **Head:** `fix/file-serving-xss-hardening`
---
### 📝 Commits (2)
- [`fd83612`](https://github.com/open-webui/open-webui/commit/fd8361217fd72c0d2209cb9bfba5ed611f7fbb2d) security: harden file serving endpoints against stored XSS
- [`addc526`](https://github.com/open-webui/open-webui/commit/addc526be0fc314c792011361fd8c017394f196f) 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
<!--
⚠️ CRITICAL CHECKS FOR CONTRIBUTORS (READ, DON'T DELETE) ⚠️
1. Target the `dev` branch. PRs targeting `main` will be automatically closed.
2. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
-->
# Pull Request Checklist
**Before submitting, make sure you've checked the following:**
- [x] **Target branch:** Verify that the pull request targets the `dev` branch.
- [x] **Description:** Provided below.
- [x] **Changelog:** Included below.
- [ ] **Documentation:** No user-facing config changes; no docs needed.
- [x] **Dependencies:** No new dependencies.
- [x] **Testing:** Manually verified that PDFs, images, and text/plain files still serve inline. HTML and SVG files are now forced to download. The `/content/html` endpoint still renders HTML but with CSP sandbox blocking script execution.
- [x] **Agentic AI Code:** This PR has gone through human review and manual testing.
- [x] **Code review:** Self-reviewed; also ran an agent code review that caught SVG in the allowlist (fixed) and dead code (cleaned up).
- [x] **Design & Architecture:** No new settings or UI changes.
- [x] **Git Hygiene:** Atomic PR, rebased on `dev`, two clean commits.
- [x] **Title Prefix:** `fix:`
# Changelog Entry
### Description
Harden file-serving endpoints against stored XSS by validating content types against a safe allowlist and adding security headers. Previously, an attacker could upload a file with `Content-Type: text/html` containing malicious scripts, and any authenticated user viewing the file would execute the scripts under the app's origin.
### Added
- `SAFE_INLINE_CONTENT_TYPES` allowlist for content types safe to serve inline (images, PDF, plain text, audio, video)
- `_safe_content_type()` helper that validates and normalizes content types against the allowlist
### Changed
- `GET /{id}/content` now validates content type against allowlist; unsafe types forced to `application/octet-stream` + attachment download
- `GET /{id}/content/html` now includes `Content-Security-Policy: sandbox` header blocking script execution
- `GET /{id}/content/{file_name}` now explicitly sets `media_type=application/octet-stream`
- All three endpoints now include `X-Content-Type-Options: nosniff`
### Deprecated
- N/A
### Removed
- Removed duplicate variable assignments (dead code cleanup)
### Fixed
- **Stored XSS via user-controlled Content-Type**: uploading files with `Content-Type: text/html` no longer results in inline HTML rendering
- **Stored XSS via HTML preview endpoint**: admin-uploaded HTML files can no longer execute scripts when viewed by other users
- **SVG script execution**: SVGs served as direct document responses can no longer execute JavaScript (excluded from inline allowlist)
### Security
- Content-type allowlist prevents browsers from executing uploaded files as HTML/JavaScript
- CSP sandbox on HTML preview endpoint blocks script execution
- `X-Content-Type-Options: nosniff` prevents MIME-sniffing attacks
- MIME parameters stripped before allowlist comparison to prevent bypasses
### Breaking Changes
- SVG files served via `/{id}/content` will now download instead of displaying inline. SVGs loaded via `<img>` tags in the frontend are unaffected since browsers disable scripting in that context.
- Files with exotic content types (e.g. `text/html`, `application/javascript`) will now force-download instead of rendering inline.
---
### Additional Information
- The `/content/html` endpoint is already restricted to files owned by admin-role users (line 724 check), but in multi-admin deployments a compromised admin could target other admins. The CSP sandbox provides defense-in-depth.
- The `users.py` and `channels.py` profile image endpoints also decode data URIs with user-controlled media types — these are out of scope for this PR but could be addressed separately.
### Contributor License Agreement
<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.
Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA.
-->
- [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
---
<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/24373
Author: @SebTardif
Created: 5/5/2026
Status: ❌ Closed
Base:
dev← Head:fix/file-serving-xss-hardening📝 Commits (2)
fd83612security: harden file serving endpoints against stored XSSaddc526security: 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
Pull Request Checklist
Before submitting, make sure you've checked the following:
devbranch./content/htmlendpoint still renders HTML but with CSP sandbox blocking script execution.dev, two clean commits.fix:Changelog Entry
Description
Harden file-serving endpoints against stored XSS by validating content types against a safe allowlist and adding security headers. Previously, an attacker could upload a file with
Content-Type: text/htmlcontaining malicious scripts, and any authenticated user viewing the file would execute the scripts under the app's origin.Added
SAFE_INLINE_CONTENT_TYPESallowlist for content types safe to serve inline (images, PDF, plain text, audio, video)_safe_content_type()helper that validates and normalizes content types against the allowlistChanged
GET /{id}/contentnow validates content type against allowlist; unsafe types forced toapplication/octet-stream+ attachment downloadGET /{id}/content/htmlnow includesContent-Security-Policy: sandboxheader blocking script executionGET /{id}/content/{file_name}now explicitly setsmedia_type=application/octet-streamX-Content-Type-Options: nosniffDeprecated
Removed
Fixed
Content-Type: text/htmlno longer results in inline HTML renderingSecurity
X-Content-Type-Options: nosniffprevents MIME-sniffing attacksBreaking Changes
/{id}/contentwill now download instead of displaying inline. SVGs loaded via<img>tags in the frontend are unaffected since browsers disable scripting in that context.text/html,application/javascript) will now force-download instead of rendering inline.Additional Information
/content/htmlendpoint is already restricted to files owned by admin-role users (line 724 check), but in multi-admin deployments a compromised admin could target other admins. The CSP sandbox provides defense-in-depth.users.pyandchannels.pyprofile image endpoints also decode data URIs with user-controlled media types — these are out of scope for this PR but could be addressed separately.Contributor License Agreement
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.