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
103 lines
3.3 KiB
TypeScript
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()
|
|
}
|