mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
security: address silent-failure review findings on top of batch 1
Second-pass fixes caught by a self-review after the initial commit — each one would have undermined a fix from the previous commit. - mfaPolicy now goes through `verifyJwtAndLoadUser` too. Without this, a JWT stolen before a password reset still satisfied `require_mfa` until its natural 24h expiry, defeating the whole point of the password_version bump. - Drop the `?? keys[0]` fallback in OIDC JWKS key selection. When the token carries a `kid` that is not in the current JWKS, refuse outright instead of picking an arbitrary key and letting the signature check produce a generic failure — the real failure mode deserves a specific error code. - Tighten OAuth DCR custom-scheme rule so `javascript:`, `data:`, `vbscript:`, `file:`, `blob:`, `about:`, `chrome:` are all rejected. Previously the catch-all "not http/https" check admitted them; the authorize flow later 302s the browser to whatever is registered, which with a `javascript:` URI would execute attacker script on redirect. Also require the private-use scheme body to be reverse-DNS (contain a dot), matching RFC 8252 §7.1. - permanentDeleteFile / emptyTrash only delete the trip_files row when the on-disk unlink actually succeeded. Previously Promise.all swallowed individual unlink failures and DELETE ran unconditionally, so a permission / ENOSPC failure would orphan bytes on disk. - restoreFromZip also invalidates the permissions cache in the outer catch. If extraction threw before the DB swap even started, the cache wasn't stale, but belt-and-braces is cheap and guarantees no failed-restore path leaves stale cache behind.
This commit is contained in:
@@ -206,16 +206,28 @@ oauthPublicRouter.post('/oauth/register', dcrLimiter, (req: Request, res: Respon
|
||||
return res.status(400).json({ error: 'invalid_redirect_uri', error_description: 'redirect_uris is required and must be a non-empty array' });
|
||||
}
|
||||
// OAuth 2.1 + RFC 8252: confidential web apps need HTTPS; public
|
||||
// clients (MCP, native) are limited to loopback or custom schemes.
|
||||
// This rejects `http://evil.example` DCR payloads that today would
|
||||
// otherwise be accepted since we previously only checked shape.
|
||||
// clients (MCP, native) are limited to loopback or a reverse-DNS
|
||||
// private-use scheme. This rejects `http://evil.example` DCR payloads
|
||||
// that today would otherwise be accepted since we previously only
|
||||
// checked shape. Dangerous URL schemes (`javascript:`, `data:` etc.)
|
||||
// are explicitly rejected — the authorize flow later 302s the
|
||||
// browser to this URI, which with `javascript:` would execute
|
||||
// attacker-controlled script under our redirect origin's context.
|
||||
const DANGEROUS_SCHEMES = new Set([
|
||||
'javascript:', 'data:', 'vbscript:', 'file:', 'blob:', 'about:', 'chrome:', 'chrome-extension:',
|
||||
]);
|
||||
const allowed = redirectUris.every((u) => {
|
||||
try {
|
||||
const url = new URL(u);
|
||||
if (DANGEROUS_SCHEMES.has(url.protocol)) return false;
|
||||
if (url.protocol === 'https:') return true;
|
||||
if (url.protocol === 'http:' && (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]')) return true;
|
||||
// RFC 8252 custom scheme for native/MCP clients (e.g. "myapp://cb")
|
||||
if (!/^https?:$/.test(url.protocol) && url.protocol.endsWith(':') && !url.protocol.includes(' ')) return true;
|
||||
// RFC 8252 §7.1 private-use scheme: must be a reverse-DNS name
|
||||
// (e.g. `com.example.myapp:/callback`). Requiring a dot in the
|
||||
// scheme is a cheap heuristic that rules out bare `myapp:` and
|
||||
// `x:` one-off schemes the spec explicitly discourages.
|
||||
const schemeBody = url.protocol.slice(0, -1);
|
||||
if (/^[a-z][a-z0-9+.-]*$/i.test(schemeBody) && schemeBody.includes('.')) return true;
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user