Skip to content

Commit d2e7440

Browse files
baseballyamaautofix-ci[bot]yusukebe
authored
feat(csrf): Support async IsAllowedSecFetchSiteHandler (#4559)
* feat: support async IsAllowedSecFetchSiteHandler * remove return type * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Yusuke Wada <[email protected]>
1 parent 489afe6 commit d2e7440

File tree

2 files changed

+48
-4
lines changed

2 files changed

+48
-4
lines changed

src/middleware/csrf/index.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,5 +471,46 @@ describe('CSRF by Middleware', () => {
471471
expect(res.status).toBe(403)
472472
})
473473
})
474+
475+
describe('async IsAllowedSecFetchSiteHandler', () => {
476+
const app = new Hono()
477+
app.use(
478+
'*',
479+
csrf({
480+
secFetchSite: async (secFetchSite, c) => {
481+
await new Promise((r) => setTimeout(r, 10))
482+
if (secFetchSite === 'same-origin') return true
483+
if (c.req.path.startsWith('/webhook/')) return true
484+
return false
485+
},
486+
})
487+
)
488+
app.post('/form', simplePostHandler)
489+
app.post('/webhook/test', simplePostHandler)
490+
491+
it('should use async custom logic for allowed values', async () => {
492+
const res = await app.request(
493+
'http://localhost/form',
494+
buildSimplePostRequestData({ secFetchSite: 'same-origin' })
495+
)
496+
expect(res.status).toBe(200)
497+
})
498+
499+
it('should use async custom logic for path-based bypass', async () => {
500+
const res = await app.request(
501+
'http://localhost/webhook/test',
502+
buildSimplePostRequestData({ secFetchSite: 'cross-site' })
503+
)
504+
expect(res.status).toBe(200)
505+
})
506+
507+
it('should block when async custom logic returns false', async () => {
508+
const res = await app.request(
509+
'http://localhost/form',
510+
buildSimplePostRequestData({ secFetchSite: 'cross-site' })
511+
)
512+
expect(res.status).toBe(403)
513+
})
514+
})
474515
})
475516
})

src/middleware/csrf/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ type SecFetchSite = (typeof secFetchSiteValues)[number]
1515
const isSecFetchSite = (value: string): value is SecFetchSite =>
1616
(secFetchSiteValues as readonly string[]).includes(value)
1717

18-
type IsAllowedSecFetchSiteHandler = (secFetchSite: SecFetchSite, context: Context) => boolean
18+
type IsAllowedSecFetchSiteHandler = (
19+
secFetchSite: SecFetchSite,
20+
context: Context
21+
) => boolean | Promise<boolean>
1922

2023
interface CSRFOptions {
2124
origin?: string | string[] | IsAllowedOriginHandler
@@ -120,7 +123,7 @@ export const csrf = (options?: CSRFOptions): MiddlewareHandler => {
120123
return (secFetchSite) => optsSecFetchSite.includes(secFetchSite)
121124
}
122125
})(options?.secFetchSite)
123-
const isAllowedSecFetchSite = (secFetchSite: string | undefined, c: Context) => {
126+
const isAllowedSecFetchSite = async (secFetchSite: string | undefined, c: Context) => {
124127
if (secFetchSite === undefined) {
125128
// denied always when sec-fetch-site header is not present
126129
return false
@@ -129,14 +132,14 @@ export const csrf = (options?: CSRFOptions): MiddlewareHandler => {
129132
if (!isSecFetchSite(secFetchSite)) {
130133
return false
131134
}
132-
return secFetchSiteHandler(secFetchSite, c)
135+
return await secFetchSiteHandler(secFetchSite, c)
133136
}
134137

135138
return async function csrf(c, next) {
136139
if (
137140
!isSafeMethodRe.test(c.req.method) &&
138141
isRequestedByFormElementRe.test(c.req.header('content-type') || 'text/plain') &&
139-
!isAllowedSecFetchSite(c.req.header('sec-fetch-site'), c) &&
142+
!(await isAllowedSecFetchSite(c.req.header('sec-fetch-site'), c)) &&
140143
!(await isAllowedOrigin(c.req.header('origin'), c))
141144
) {
142145
const res = new Response('Forbidden', { status: 403 })

0 commit comments

Comments
 (0)