Repository navigation
Conversation
Self-hosted deployments that supply only the JWT_JWKS env config could not sign storage URLs with an asymmetric key: urlSigningKey was auto-populated only in the multi-tenant JWKS loader, so single-tenant installs with an EC signing key silently fell back to the HMAC jwtSecret. - add isUrlSigningCapableJwk / pickUrlSigningKey helpers in config.ts (oct with k or EC with d; reject use="enc" per RFC 7517 §4.2) - parse JWT_JWKS with auto-populated urlSigningKey when the field is absent, preserving an explicit value when provided - mergeTenantJwksWithLegacyKeys now preserves the tenant urlSigningKey with a fallback picked from the merged key list - refactor jwksManager tenant loader to use the shared capability check - document the URL signing contract in .env.sample - 9 unit tests covering selection, exclusion, and backward compatibility HS256 default deployments (no JWT_JWKS) are unaffected.
…supabase#629) Adds a unit-level round-trip test that provisions JWT_JWKS with only an ES256 EC key, lets the config parser auto-populate urlSigningKey, calls the real signJWT, and verifies the token header reports alg=ES256 with the expected kid — proving asymmetric signing actually happens rather than silently falling back to the HS256 jwtSecret. Also adds a negative control that an RSA-only JWT_JWKS still leaves urlSigningKey unset (RSA cannot sign storage URLs per the maintainer whitelist) and that the keys are still parsed for verification.
Operators who provisioned JWT_JWKS without a URL-signing-capable key previously had storage URL signing silently fall back to the HMAC jwtSecret, with the only observable symptom being the signed-URL header bytes. The github issue reporter spent debugging time tracing this to the config. Now the first call that reads the single-tenant JWT config emits a one-shot startup warning describing the mismatch. - describeJwtJwksMisconfiguration helper in config.ts (pure, no logger dependency so it stays importable from the module graph that logger itself depends on) - getSingleTenantJwtConfig emits the warning via logSchema.warning once per process, keeping the fast path allocation-free afterwards - warning metadata exposes only key count, kty list, and configured urlSigningJwkType — no key material, no HMAC secret - 5 unit tests pinning the dangerous shape and the four excluded silent-safe scenarios, with an explicit red-team guard that the serialized description never carries key material HS256 default deployments and well-configured JWT_JWKS tenants stay silent.
…pabase#629) The two capability helpers are consumed by three independent code paths (JWT_JWKS env parser, tenant JWKS merge, per-tenant jwksManager loader). Previously they were only reached indirectly through the config-parsing and misconfiguration-detection tests. Add dedicated table-driven tests that pin every branch of the capability predicate (oct/EC with and without private material, RSA, OKP, use="enc" for both key types) and the ordering/empty-list semantics of the picker, so a future refactor that touches these helpers surfaces a direct failure instead of a confusing cross-module regression.
…base#639) The bucket-level allowed_mime_types check only compared the client-declared Content-Type / multipart field. A caller could rename a .gif to .jpg, send Content-Type: image/jpeg, and pass the check even though the actual bytes were a GIF (issue supabase#639, repro from flogesell, 2024). Add a magic-byte verifier that peeks the first 4 KiB of the upload stream and, when a bucket actually restricts mimetypes, rejects uploads whose detected signature does not belong to the declared MIME family. - src/storage/validators/file-signature.ts * Pure helper, zero new dependencies, no logger/db imports (avoids the circular-import pattern that bit us on supabase#629). * Covers 23 signatures spanning images, documents, archives, A/V, and executables. Refine functions distinguish RIFF/WEBP from RIFF/WAVE and the ISO BMFF ftyp sub-brands (mp4 / heic / avif / mov / 3gp / m4a). * Returns undefined on unknown signatures so novel formats keep working. * mimeFamiliesMatch is intentionally loose: image/jpg == image/jpeg, application/zip accepted for OOXML office MIMEs, x-cfb for legacy OLE2. * wrapWithSignatureDetection returns a PassThrough + a promise that resolves with the detected MIME once enough bytes have flowed. No bytes are swallowed; the backend sees the stream unmodified. - src/storage/uploader.ts * After the existing validateMimeType header check, if the bucket has allowed_mime_types set, wrap the body with the detector. On mismatch the stream is destroyed with ERRORS.InvalidMimeType, so the backend upload aborts cleanly and the client gets the same 415 as a header- level rejection. * Buckets without allowed_mime_types stay on the fast path (no wrapping). - src/storage/validators/file-signature.test.ts * 41 vitest cases: real-signature detection for each format, edge cases (empty / 1-byte / unknown prefix), RIFF/ftyp false-positive defenses, and the eight security-relevant mismatch scenarios the fix is meant to catch (renamed gif, polyglot jpg-html, exe-as-pdf, zip-as-image, …).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds magic-byte verification to the uploader so that bucket-level
allowed_mime_typesrestrictions catch files whose declared MIME does notmatch the actual signature.
Fixes #639 — the reporter's
case was uploading a renamed
.gifwithContent-Type: image/jpegto abucket restricted to
image/jpeg,image/png; today'svalidateMimeTypeaccepts it because only the client-declared value is checked.
How
Pre-existing path (unchanged):
validateMimeType(mimeType, allowedMimeTypes)— compares the declaredContent-Type / multipart field against the bucket's allowlist.
Added path (
src/storage/validators/file-signature.ts):detectMimeFromBuffer(buf)— 23 signatures covering images, documents,archives, audio/video, and executables. Refine functions distinguish
RIFF/WEBP from RIFF/WAVE, and ISO BMFF
ftypsub-brands(mp4 / heic / avif / mov / 3gp / m4a / webm). Returns
undefinedonunknown signatures so novel formats continue to work.
mimeFamiliesMatch(detected, declared)— intentionally loose:image/jpg == image/jpeg,application/zipaccepted for OOXML officeMIMEs,
application/x-cfbaccepted for legacy OLE2 formats.wrapWithSignatureDetection(source)— returns aPassThroughand apromise that resolves with the detected MIME once the first 4 KiB has
flowed. No bytes are swallowed; the backend sees the stream unmodified.
Wiring in
uploader.ts(after the existing declared-MIME check):Buckets with no mimetype restriction stay on the fast path — no wrapping,
no extra CPU, zero behavioral change.
Why this design
file-type(ESM-only incurrent versions) and keeps the supply-chain surface flat.
worked on JWK not used in signObjectURL #629 — anything circular is caught at compile time.
truncated or delayed body. Max 4 KiB extra buffered.
ERRORS.InvalidMimeType,same error code and HTTP status (415) as a header-level rejection, so
existing clients already handle it.
image/jpg→image/jpeg, zip → OOXML office,x-cfb → legacy office. Prevents false positives on legitimate uploads
while still catching the attack cases from the report.
Tests
41 vitest cases in
src/storage/validators/file-signature.test.ts:ftypwithan unknown brand (both must return
undefined, not fall back to theentry's default MIME).
the eight security-relevant mismatch scenarios the fix is meant to catch:
.gif→image/jpeg(the exact mime-type does not check uploaded files, only the filename #639 repro)text/htmlapplication/pdfimage/jpegimage/pngimage/jpegimage/jpegAlso verified
mime-type.test.tsstill passes (37/37) — no behavior changeto the existing declared-MIME check.
Run:
Impact
allowed_mime_types→ zero code-path change.PassThrough+ 4 KiB peek perupload. Negligible overhead vs. the network / storage write cost.
stands. We only refuse when we're certain the actual family is wrong.
Security notes
This closes a real file-upload hardening gap. Common attack cases this now
catches:
image/pngto abucket that only allows images, then trigger it from a victim client.
declared as
image/jpeg.Report: #639 by @flogesell,
open 853 days.
Prior work in this repo for context: