Files
carmanagement/apps/api/src/middleware/csrf.ts
T
root 50c74b9007
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
admin login fixed.
2026-07-28 22:56:18 -04:00

103 lines
3.3 KiB
TypeScript

import type { Request, Response, NextFunction } from 'express'
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
const SESSION_COOKIE_PATTERN = /(?:^|;\s*)(?:admin_session|employee_session|renter_session)=/
function configuredOrigins() {
return [
process.env.DASHBOARD_URL,
process.env.ADMIN_URL,
process.env.CARPLACE_URL,
process.env.WEBSITE_URL,
process.env.NEXT_PUBLIC_DASHBOARD_URL,
process.env.NEXT_PUBLIC_ADMIN_URL,
process.env.NEXT_PUBLIC_CARPLACE_URL,
process.env.NEXT_PUBLIC_WEBSITE_URL,
process.env.CORS_ORIGINS,
]
.flatMap((value) => (value ?? '').split(','))
.map((value) => value.trim())
.filter(Boolean)
}
function normalizeOrigin(value: string | undefined): string | null {
if (!value) return null
try {
const url = new URL(value)
return url.origin
} catch {
return null
}
}
function originFromReferer(value: string | undefined): string | null {
if (!value) return null
try {
return new URL(value).origin
} catch {
return null
}
}
function isAllowedDevelopmentOrigin(origin: string) {
if (process.env.NODE_ENV === 'production') return false
try {
const url = new URL(origin)
if (url.protocol !== 'http:') return false
const trustedDevPorts = new Set(['3000', '3001', '3002', '3004', '4000'])
if (!trustedDevPorts.has(url.port)) return false
if (['localhost', '127.0.0.1'].includes(url.hostname)) return true
const octets = url.hostname.split('.').map((part) => Number(part))
if (octets.length !== 4 || octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
return false
}
const first = octets[0]!
const second = octets[1]!
return first === 10 || (first === 172 && second >= 16 && second <= 31) || (first === 192 && second === 168)
} catch {
return false
}
}
export function isTrustedBrowserOrigin(origin: string | null) {
if (!origin) return false
const allowed = new Set(configuredOrigins().map(normalizeOrigin).filter((value): value is string => Boolean(value)))
return allowed.has(origin) || isAllowedDevelopmentOrigin(origin)
}
function isCookieAuthenticatedBrowserMutation(req: Request) {
if (!MUTATING_METHODS.has(req.method.toUpperCase())) return false
const cookie = req.headers.cookie ?? ''
if (!SESSION_COOKIE_PATTERN.test(cookie)) return false
const secFetchSite = Array.isArray(req.headers['sec-fetch-site'])
? req.headers['sec-fetch-site'][0]
: req.headers['sec-fetch-site']
if (secFetchSite && secFetchSite !== 'same-origin' && secFetchSite !== 'same-site' && secFetchSite !== 'none') return true
// Browser cookie-authenticated mutations must present Origin. Referer is a
// fallback for older clients only; API clients should use Bearer tokens.
return true
}
export function requireTrustedOriginForCookieMutations(req: Request, res: Response, next: NextFunction) {
if (!isCookieAuthenticatedBrowserMutation(req)) return next()
const origin = normalizeOrigin(req.headers.origin as string | undefined)
?? originFromReferer(req.headers.referer as string | undefined)
if (!isTrustedBrowserOrigin(origin)) {
return res.status(403).json({
error: 'csrf_origin_rejected',
message: 'Mutating cookie-authenticated requests must come from a trusted application origin.',
statusCode: 403,
})
}
next()
}