Skip to content

Commit 748b82b

Browse files
committed
perf(router): skip known-empty security probes
1 parent 4322d42 commit 748b82b

3 files changed

Lines changed: 32 additions & 10 deletions

File tree

storage/framework/core/router/src/security-headers.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,25 +77,27 @@ function resolveCsp(): { header: string, value: string } | null {
7777
*
7878
* Skips overwriting any header that's already set — explicit userland
7979
* config wins. Skips entirely when `STACKS_SECURITY_HEADERS_DISABLE=true`.
80+
* `knownMissing` is reserved for router-created responses whose security
81+
* headers are known to be absent, avoiding redundant native lookups.
8082
*/
81-
export function applySecurityHeaders(headers: Headers): void {
83+
export function applySecurityHeaders(headers: Headers, knownMissing = false): void {
8284
if (isDisabled())
8385
return
8486

85-
if (headers.get('X-Content-Type-Options') === null)
87+
if (knownMissing || headers.get('X-Content-Type-Options') === null)
8688
headers.set('X-Content-Type-Options', 'nosniff')
8789

88-
if (headers.get('X-Frame-Options') === null)
90+
if (knownMissing || headers.get('X-Frame-Options') === null)
8991
headers.set('X-Frame-Options', 'SAMEORIGIN')
9092

91-
if (headers.get('Referrer-Policy') === null)
93+
if (knownMissing || headers.get('Referrer-Policy') === null)
9294
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
9395

94-
if (isProduction() && headers.get('Strict-Transport-Security') === null)
96+
if (isProduction() && (knownMissing || headers.get('Strict-Transport-Security') === null))
9597
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
9698

9799
const csp = resolveCsp()
98-
if (csp && headers.get(csp.header) === null)
100+
if (csp && (knownMissing || headers.get(csp.header) === null))
99101
headers.set(csp.header, csp.value)
100102
}
101103

storage/framework/core/router/src/stacks-router.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ const routeActionRegistry = new Map<string, RouterAction>()
603603
/** HTTP methods that mutate state and therefore need CSRF protection. */
604604
const CSRF_PROTECTED_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
605605
const CSRF_SEEDED_BY_HANDLE_REQUEST = Symbol.for('stacks.router.csrfSeededByHandleRequest')
606+
const FRAMEWORK_SECURITY_HEADERS_MISSING = Symbol('stacks.router.frameworkSecurityHeadersMissing')
606607

607608
interface ResolvedMiddleware {
608609
name: string
@@ -2015,7 +2016,9 @@ function createMiddlewareHandler(routeKey: string, handler: StacksHandler): Rout
20152016
}
20162017
h.set('Server-Timing', timing)
20172018
}
2018-
applySecurityHeaders(h)
2019+
const frameworkHeadersAreMissing = (response as unknown as Record<symbol, unknown>)[FRAMEWORK_SECURITY_HEADERS_MISSING] === true
2020+
&& !requested
2021+
applySecurityHeaders(h, frameworkHeadersAreMissing)
20192022
}
20202023

20212024
if (response && typeof (response).headers?.set === 'function') {
@@ -2936,8 +2939,11 @@ function formatResult(result: unknown, req: EnhancedRequest): Response {
29362939
/** Serialize JSON once and make its size available to compression. */
29372940
function formatJsonResult(result: unknown, req: EnhancedRequest, linkHeader?: string | null): Response {
29382941
const encoding = req.headers.get('accept-encoding')
2939-
if (!encoding || encoding === 'identity')
2940-
return linkHeader ? Response.json(result, { headers: { Link: linkHeader } }) : Response.json(result)
2942+
if (!encoding || encoding === 'identity') {
2943+
const response = linkHeader ? Response.json(result, { headers: { Link: linkHeader } }) : Response.json(result)
2944+
;(response as unknown as Record<symbol, unknown>)[FRAMEWORK_SECURITY_HEADERS_MISSING] = true
2945+
return response
2946+
}
29412947

29422948
// Give compression the byte length without making it consume and rebuild
29432949
// the response stream just to check its threshold. Count UTF-8 bytes, not
@@ -2949,7 +2955,9 @@ function formatJsonResult(result: unknown, req: EnhancedRequest, linkHeader?: st
29492955
}
29502956
if (linkHeader)
29512957
headers.Link = linkHeader
2952-
return new Response(body, { headers })
2958+
const response = new Response(body, { headers })
2959+
;(response as unknown as Record<symbol, unknown>)[FRAMEWORK_SECURITY_HEADERS_MISSING] = true
2960+
return response
29532961
}
29542962

29552963
/**

storage/framework/core/router/tests/response-headers.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ beforeAll(async () => {
3636
return new Response('ok', { status: 200 })
3737
})
3838

39+
route.get('/_mw_headers_security_override', (request: any) => {
40+
request._responseHeaders = { 'X-Frame-Options': 'DENY' }
41+
42+
return { ok: true }
43+
})
44+
3945
server = await route.serve({ port: 0, hostname: '127.0.0.1' })
4046
port = Number(server?.port ?? server?.server?.port ?? 0)
4147
})
@@ -69,4 +75,10 @@ describe('_responseHeaders', () => {
6975

7076
expect(answer.headers.get('X-Request-ID')).toBe('the-router-owns-this')
7177
})
78+
79+
it('can override a security default on a framework-created response', async () => {
80+
const answer = await fetch(`http://127.0.0.1:${port}/_mw_headers_security_override`)
81+
82+
expect(answer.headers.get('X-Frame-Options')).toBe('DENY')
83+
})
7284
})

0 commit comments

Comments
 (0)