Skip to content

Fix/upload storage guardrails#14129

Open
lurkerCha wants to merge 7 commits into
danny-avila:mainfrom
lurkerCha:fix/upload-storage-guardrails
Open

Fix/upload storage guardrails#14129
lurkerCha wants to merge 7 commits into
danny-avila:mainfrom
lurkerCha:fix/upload-storage-guardrails

Conversation

@lurkerCha

@lurkerCha lurkerCha commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two small, non-breaking hardening changes to the file upload/storage path. Both replace a "trust the client-supplied label" check with a "verify the actual value" check.

1. Content sniffing on upload (filterFile)

filterFile gated uploads on the browser-supplied file.mimetype/filename, which are attacker-controlled — a file could claim an allowed type while its bytes are something else. After the existing
size/claimed-type checks, it now sniffs the saved file's magic bytes (via file-type, already a dependency) and enforces:

  • Executables are always rejected (ELF, PE, Mach-O, WASM, Java) regardless of endpoint.
  • The sniffed content must match the claimed category, so a document or executable disguised as an image/media — or media disguised as a document — is rejected even when the claimed type is otherwise allowed.
  • For detectable binary types (image, PDF, ZIP, OOXML/ODF, legacy Office), the sniffed type must also be permitted by the endpoint allow-list, so an unsupported type can't ride in under an allowed claim (e.g.
    image/bmp as image/png, or a raw ZIP as application/pdf).

filterFile becomes async; all call sites now await it.

Scope/limitations of magic-byte sniffing (by design):

  • Media sub-types are validated by category, not exact allow-list match: file-type's canonical names diverge from the configured tokens (video/quicktime vs video/mov, video/matroska vs video/mkv,
    Ogg/ASF reported under application/*) and containers don't reveal audio-vs-video, so an exact check would falsely reject legitimate .mov/.mkv/.ogg/.wmv uploads. Executable/cross-category disguises are
    still blocked for media.
  • Text-based content (HTML/SVG/CSV/code) has no magic bytes, so it falls through to the existing claimed-type allow-list gate (unchanged).

2. Firebase delete validates structured owner, not URL substring

deleteFirebaseFile guarded deletion with filepath.includes(req.user.id) — a substring check against the persisted URL. It now:

  • validates the file record's structured owner (file.user) at its fixed path position (${basePath}/${ownerId}/${fileName}), rejecting crafted paths like images/victim/${ownerId}/secret.png, and
  • runs the RAG cleanup (deleteRagFile) under the file owner rather than the requester, and only after ownership is validated — so a shared-agent editor deleting an owner's file removes the owner's vectors.

No new dependencies, env vars, interface changes, or version bumps. filterFile's call signature is unchanged ({ req, image, isAvatar }); it just returns a promise now.

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

Added/updated unit tests for both changes, and verified the sniffer strings the tests use match real file-type output for real files of each type (file-type is ESM and can't load under Jest, so the sniffer is
mocked with values confirmed against real files).

  • Content sniffingprocess.spec.js: cross-category spoofs rejected (PDF-as-image); unsupported-but-allowed-claim types rejected (BMP-as-PNG, ZIP-as-PDF); executables rejected; genuine
    images/PDF/Office/legacy-Office/media (including .mov/.3gp/.ogg/.wmv) accepted; disguises within media rejected (PDF-as-.mov, .mov-as-PDF); non-sniffable content still passes the claimed-type gate.
  • Firebase deleteFirebase/crud.spec.js: same-owner delete succeeds; cross-user path rejected; substring-not-segment owner rejected; wrong-position owner path rejected; missing-owner rejected; RAG cleanup
    runs under the file owner.

Run:
cd api && npx jest server/services/Files/process.spec.js server/services/Files/Firebase/crud.spec.js
Lint clean on all touched files.

Test Configuration:

  • Node.js v24.16.0
  • Backend Jest unit tests (api workspace); no external services required (Firebase SDK boundary is mocked, real path/owner-validation logic is exercised).

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

lurkerCha and others added 2 commits July 6, 2026 13:38
`filterFile` gated uploads on the browser-supplied `file.mimetype`/filename,
which are attacker-controlled, so a file could claim an allowed type while its
bytes were something else. After the size/claimed-type checks, sniff the saved
file's magic bytes and require the detected type to also pass the endpoint's
allow-list; files with no detectable signature (plain text, code, csv, svg)
fall through to the existing claimed-type gate. `filterFile` becomes async and
all call sites now await it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`deleteFirebaseFile` guarded deletion with `filepath.includes(req.user.id)`, a
substring check against the persisted URL. Parse the storage path and require
the file record's structured owner (`file.user`) to match as an exact path
segment, so a crafted filepath cannot target another user's object.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46a83ff3e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/services/Files/process.js Outdated
return;
}

if (!fileConfig.checkType(detected.mime, endpointFileConfig.supportedMimeTypes)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require sniffed MIME to match the upload type

When an endpoint's allow-list is broad (the default config allows both images and PDFs), this check still accepts a PDF uploaded with a claimed image/png MIME because application/pdf is also allowed by the endpoint. Downstream regular uploads still persist type: file.mimetype (processFileUpload stores the browser-supplied value), and image/avatar routes are likewise relying on this guard to reject spoofed content, so the new MIME-spoofing protection is bypassed for default/broad endpoint configs instead of verifying that the detected type matches the claimed/upload context.

Useful? React with 👍 / 👎.

const deleteFirebaseFile = async (req, file) => {
await deleteRagFile({ userId: req.user.id, file });

const ownerId = file.user?.toString?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the file owner for Firebase RAG cleanup

When a user with EDIT access to a shared agent deletes an embedded Firebase file owned by the agent author, the new owner-based path validation below allows deleting that non-owned storage object, but deleteRagFile has already run with req.user.id instead of the file owner. Since the RAG API token is generated for the supplied user, the owner's embedded document is not removed while the Firebase object and DB metadata can still be deleted, leaving orphaned vectors for shared-agent Firebase files.

Useful? React with 👍 / 👎.

Comment thread api/server/services/Files/process.js Outdated
return;
}

if (!fileConfig.checkType(detected.mime, endpointFileConfig.supportedMimeTypes)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize detected MIME aliases before rejecting uploads

When users upload otherwise-supported WAV or AVI files, file-type reports the magic bytes with vendor MIME names such as audio/vnd.wave / video/vnd.avi, while the default allow-list in packages/data-provider/src/file-config.ts accepts audio/wav/audio/x-wav and video/avi. Comparing detected.mime directly here makes those legitimate uploads fail with File content does not match its file type; normalize the sniffer result (or include these aliases) before applying the allow-list.

Useful? React with 👍 / 👎.

const fileName = extractFirebaseFilePath(file.filepath);
if (!fileName.includes(req.user.id)) {
throw new Error('Invalid file path');
if (!fileName || !fileName.split('/').includes(ownerId)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the owner segment at the expected path position

This accepts any Firebase object path that contains the owner id as some segment, but Firebase uploads in this service are written as ${basePath}/${userId}/${fileName}. If a persisted URL is ever crafted as images/victim/${ownerId}/secret.png, it passes this check for file.user === ownerId and deletes that arbitrary object; validate the parsed path shape/prefix instead of searching all segments.

Useful? React with 👍 / 👎.

Content sniffing: compare the sniffed type against the claimed `file.mimetype`
instead of the endpoint allow-list. On broad endpoint configs the previous check
accepted e.g. a PDF claimed as `image/png` because PDF was also allowed; now a
mismatch in content category is rejected and executables are rejected outright.
Category comparison (audio/video merged for shared containers) tolerates benign
sniffer aliases so legitimate WAV/AVI/legacy-Office uploads are not falsely
rejected.

Firebase delete: run RAG cleanup under the file's owner (`file.user`) rather than
the requester, and only after ownership is validated, so a shared-agent editor
deleting an owner's file removes the owner's vectors. Validate the owner id at
its fixed path position (second segment) instead of searching all segments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danny-avila

Copy link
Copy Markdown
Owner

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: afa44ea4e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/services/Files/process.js Outdated
Comment on lines +1316 to +1318
if (contentCategory(detectedMime) !== contentCategory(claimedMime)) {
throw new Error('File content does not match its file type');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject detected MIME types outside the allow-list

When an endpoint narrows supportedMimeTypes within a top-level category (for example PDF-only), this comparison still accepts an unsupported same-category upload such as bytes detected as application/zip while claiming application/pdf, because both collapse to application. The earlier check in filterFile has already trusted file.mimetype, so the sniffed MIME needs to be checked against endpointFileConfig.supportedMimeTypes (with explicit aliases where needed) rather than only comparing categories; otherwise the new spoofing guard can be bypassed by unsupported same-category content.

Useful? React with 👍 / 👎.

The category-only comparison accepted an unsupported same-category upload (e.g.
bytes detected as application/zip while claimed as application/pdf), since both
collapse to `application`. Compare a container-aware content signature instead:
application/* is kept fine-grained so zip never matches pdf, while ZIP-based
(OOXML/ODF/epub, sniffed as application/zip) and OLE/CFB (legacy Office, sniffed
as application/x-cfb) container families are each treated as one signature so
genuine Office/OpenDocument uploads still pass. The claimed type is already
validated against the endpoint allow-list, so requiring the sniffed content to
match the claim also enforces the allow-list against the real bytes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lurkerCha

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d151d98852

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread api/server/services/Files/process.js Outdated
Comment on lines +1374 to +1376
if (contentSignature(detectedMime) !== contentSignature(claimedMime)) {
throw new Error('File content does not match its file type');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check detected MIME against the allow-list

For endpoints with a narrower supportedMimeTypes list, this comparison still accepts unsupported bytes whenever they share a contentSignature with the claimed type. For example, the default config rejects image/bmp, but a BMP uploaded as image/png reaches this line with both signatures equal to image, so filterFile passes even though the detected MIME never matched the endpoint allow-list; the same pattern applies to ZIP containers claimed as OOXML. Please validate detectedMime against endpointFileConfig.supportedMimeTypes rather than only comparing it with the client-supplied MIME.

Useful? React with 👍 / 👎.

The signature-only comparison accepted an unsupported type whenever it shared a
signature with the (allowed) claimed type — e.g. image/bmp claimed as image/png
on an image/png-only endpoint, since both are `image`. Also check the sniffed
type itself against endpointFileConfig.supportedMimeTypes (via checkType), so a
type the endpoint disallows cannot ride in under an allowed claim.

The claimed-signature comparison is kept too, so an allowed-but-different type
(a PDF claimed as image/png on an endpoint that also allows PDF) is still
rejected. `file-type` reports genuine OOXML/ODF/epub uploads as their specific
type, so a plain application/zip detection is a real ZIP and only passes where
ZIP is allowed — closing the raw-ZIP-claimed-as-OOXML bypass. Legacy Office
(.doc/.xls/.ppt) all report application/x-cfb and cannot be told apart, so the
OLE family is expanded to avoid rejecting genuine legacy Office uploads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lurkerCha

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7567764e07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const allowListCandidates = (detectedMime) => {
const candidates = new Set([detectedMime]);
const [type, subtype = ''] = detectedMime.split('/');
candidates.add(`${type}/${subtype.replace(/^x-/, '').replace(/^vnd\./, '')}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map sniffer aliases before allow-listing

When a valid upload uses one of the media types already allowed by LibreChat, file-type can report a different canonical MIME than the configured allow-list: for example a .mov upload can be claimed/accepted as video/mov by the existing config (packages/data-provider/src/file-config.ts:198) but is sniffed as video/quicktime. Since this candidate set only strips leading x-/vnd. and does not add equivalents such as video/mov, detectedIsSupported becomes false and the upload is now rejected as spoofed even though it previously matched the configured supported type.

Useful? React with 👍 / 👎.

`file-type` reports canonical media names that diverge from the configured
allow-list tokens (video/quicktime vs video/mov, video/matroska vs video/mkv,
Ogg/ASF under application/*), so the exact sniffed-type allow-list check falsely
rejected legitimate .mov/.3gp/.ogg/.wmv uploads. Recognize the application/*
media containers as media and skip the exact allow-list check for media,
constraining it to the same category as the (already allow-listed) claim. The
executable rejection and the claim-signature check are unchanged, so a document
or executable disguised as media — and media disguised as a document — are still
rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lurkerCha

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 4a74508fcf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants