Security: harden auth, dlimit enforcement, and crypto#1
Merged
Conversation
- keychain: raise PBKDF2 iterations from 100 to 100000 for password protected file auth-key derivation - config: declare fxa_required (FXA_REQUIRED) so the auth gate in middleware/auth.js and routes/ws.js is actually honored - download: atomically claim a dl slot via HINCRBY before streaming so concurrent requests sharing an Authorization header cannot exceed dlimit; refund on cancel or error - auth middleware: await nonce rotation write before responding - upload + ws: reject Authorization headers that are not send-v1 <base64>, preventing a stored "undefined" auth key - params + ws: require dlimit to be a positive integer <= max - storage: make setField/incrementField return promises (promisified hset/hincrby) so callers can await - s3 storage: pass endpoint/path-style config to new AWS.S3(cfg) instead of mutating global AWS.config
This was referenced May 22, 2026
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.
Summary
Security audit of the server-side code surfaced several concrete, exploitable issues. This PR fixes them. See per-file notes below.
Crypto
app/keychain.js— password-protected files derived the HMAC auth key with PBKDF2-HMAC-SHA-256 / 100 iterations. Raised to 100,000 (1000× factor). OWASP currently recommends ≥600k; 100k is a UX-balanced jump that nonetheless removes the offline-brute-force trivial-cost concern. Older password-protected links uploaded by previous clients will not be auth-compatible with the new client; since files expire within at most 7 days this is acceptable.Auth gating actually honored
server/config.js—config.fxa_requiredwas referenced inmiddleware/auth.js:75androutes/ws.js:37but never declared in the convict schema, soFXA_REQUIRED=truewas silently a no-op. Added the key with envFXA_REQUIRED.Download-count enforcement (TOCTOU)
server/routes/download.js+server/middleware/auth.js— N concurrent download requests sharing oneAuthorizationheader all read the samemeta.dl, all streamed, then all incremented the counter afterward — effectively bypassingdlimit. Moved counting to an atomicHINCRBYbefore streaming, with refund on stream error / client cancel / setup failure. Alsoawaits the nonce-rotation write in the HMAC middleware.Authorization-header strictness
server/routes/upload.js+server/routes/ws.js— a malformed header (no space) madeauth.split(' ')[1]evaluate toundefined, which Redis serialized as the literal string"undefined". The HMAC verification key then becameBuffer.from("undefined", "base64")— a known, short, deterministic buffer any party could forge against. Now both endpoints requiresend-v1 <base64>and reject otherwise.Input validation on download count
server/routes/params.js+server/routes/ws.js—dlimitaccepted negatives, floats, and non-numbers. A negative value made the first download immediately delete the file. Now requiresNumber.isInteger(dlimit) && 1 <= dlimit <= max_downloads.S3 storage isolation
server/storage/s3.js—AWS.config.update(cfg)mutated the SDK config process-wide, potentially redirecting unrelated AWS service clients to a custom S3 endpoint. Now passescfgtonew AWS.S3(cfg)only.Plumbing
server/storage/redis.js— added promisifiedhsetAsync/hincrbyAsync.server/storage/index.js—setField/incrementFieldnow return promises so race-sensitive callers canawait.Reviewed and intentionally not changed
DETECT_BASE_URL=true— opt-in operator footgun.base_urldefault ofhttps://send.example.com— misconfiguration, not a vulnerability.verify()delegating to a remote/verifyinstead of local JWT signature verification — design tied to the upstream FxA flow; would need a JWKS-based verifier (out of scope here).storage/fs.js— adequately gated by route-level[0-9a-fA-F]{10,16}regexes and server-prepended prefix.keychain.encryptMetadata— safe because the key (rawSecret) is randomized per file.Test plan
npm install && npm run lintnpm run test:backenddlimitreached.dlimit=1, issue two parallelGET /api/download/:idrequests sharing the same freshAuthorizationheader — exactly one should succeed.FXA_REQUIRED=truewithFXA_CLIENT_IDconfigured; anonymous upload should 401.Authorization: footoPOST /api/upload— should 400.