Repository: honojs/hono
File: src/middleware/csrf/index.ts (TypeScript)

Code before:
/**
 * @module
 * CSRF Protection Middleware for Hono.
 */

import type { Context } from '../../context'
import { HTTPException } from '../../http-exception'
import type { MiddlewareHandler } from '../../types'

type IsAllowedOriginHandler = (origin: string, context: Context) => boolean
type IsAllowedSecFetchSiteHandler = (secFetchSite: string, context: Context) => boolean
interface CSRFOptions {
  origin?: string | string[] | IsAllowedOriginHandler
  secFetchSite?: string | string[] | IsAllowedSecFetchSiteHandler
}

const isSafeMethodRe = /^(GET|HEAD)$/
const isRequestedByFormElementRe =
  /^\b(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)\b/i

/**
 * CSRF Protection Middleware for Hono.
 *
 * Protects against Cross-Site Request Forgery attacks by validating request origins
 * and sec-fetch-site headers. The request is allowed if either validation passes.
 *
 * @see {@link https://hono.dev/docs/middleware/builtin/csrf}
 *
 * @param {CSRFOptions} [options] - The options for the CSRF protection middleware.
 * @param {string|string[]|(origin: string, context: Context) => boolean} [options.origin] -
 *   Allowed origins for requests.
 *   - string: Single allowed origin (e.g., 'https://example.com')
 *   - string[]: Multiple allowed origins
 *   - function: Custom validation logic
 *   - Default: Only same origin as the request URL
 * @param {string|string[]|(secFetchSite: string, context: Context) => boolean} [options.secFetchSite] -
 *   Sec-Fetch-Site header validation. Standard values include 'same-origin', 'same-site', 'cross-site', 'none'.
 *   - string: Single allowed value (e.g., 'same-origin')
 *   - string[]: Multiple allowed values (e.g., ['same-origin', 'same-site'])

Review comment (security):
The `string` should be only `same-origin`, `same-site`, `none`, or `cross-site`, right? If so, (this is a matter of test), you can define types like the following to improve the DX:

```ts
const secFetchSiteValues = ['same-origin', 'same-site', 'none', 'cross-site'] as const
type SecFetchSite = (typeof secFetchSiteValues)[number]

// ...

interface CSRFOptions {
  origin?: string | string[] | IsAllowedOriginHandler
  secFetchSite?: SecFetchSite | SecFetchSite[] | IsAllowedSecFetchSiteHandler
}
```

<img width="1310" height="592" alt="CleanShot 2025-08-18 at 17 38 40@2x" src="https://github.com/user-attachments/assets/a9a5a8f6-e5f9-44ae-924e-7197f189a188" />

One of example codes is like the following:

```diff
diff --git a/src/middleware/csrf/index.ts b/src/middleware/csrf/index.ts
index 6ce35862..70970a34 100644
--- a/src/middleware/csrf/index.ts
+++ b/src/middleware/csrf/index.ts
@@ -8,10 +8,18 @@ import { HTTPException } from '../../http-exception'
 import type { MiddlewareHandler } from '../../types'

 type IsAllowedOriginHandler = (origin: string, context: Context) => boolean
-type IsAllowedSecFetchSiteHandler = (secFetchSite: string, context: Context) => boolean
+
+const secFetchSiteValues = ['same-origin', 'same-site', 'none', 'cross-site'] as const
+type SecFetchSite = (typeof secFetchSiteValues)[number]
+
+const isSecFetchSite = (value: string): value is SecFetchSite => {
+  return (secFetchSiteValues as readonly string[]).includes(value)
+}
+
+type IsAllowedSecFetchSiteHandler = (secFetchSite: SecFetchSite, context: Context) => boolean
 interface CSRFOptions {
   origin?: string | string[] | IsAllowedOriginHandler
-  secFetchSite?: string | string[] | IsAllowedSecFetchSiteHandler
+  secFetchSite?: SecFetchSite | SecFetchSite[] | IsAllowedSecFetchSiteHandler
 }

 const isSafeMethodRe = /^(GET|HEAD)$/
@@ -117,6 +125,10 @@ export const csrf = (options?: CSRFOptions): MiddlewareHandler => {
       // denied always when sec-fetch-site header is not present
       return false
     }
+    // Type guard to check if the value is a valid SecFetchSite
+    if (!isSecFetchSite(secFetchSite)) {
+      return false
+    }
     return secFetchSiteHandler(secFetchSite, c)
   }
```

What do you think of this? Or, is this over-TypeScripted?

Code after:
/**
 * @module
 * CSRF Protection Middleware for Hono.
 */

import type { Context } from '../../context'
import { HTTPException } from '../../http-exception'
import type { MiddlewareHandler } from '../../types'

type IsAllowedOriginHandler = (origin: string, context: Context) => boolean

const secFetchSiteValues = ['same-origin', 'same-site', 'none', 'cross-site'] as const
type SecFetchSite = (typeof secFetchSiteValues)[number]

const isSecFetchSite = (value: string): value is SecFetchSite =>
  (secFetchSiteValues as readonly string[]).includes(value)

type IsAllowedSecFetchSiteHandler = (secFetchSite: SecFetchSite, context: Context) => boolean

interface CSRFOptions {
  origin?: string | string[] | IsAllowedOriginHandler
  secFetchSite?: SecFetchSite | SecFetchSite[] | IsAllowedSecFetchSiteHandler
}

const isSafeMethodRe = /^(GET|HEAD)$/
const isRequestedByFormElementRe =
  /^\b(application\/x-www-form-urlencoded|multipart\/form-data|text\/plain)\b/i

/**
 * CSRF Protection Middleware for Hono.
 *
 * Protects against Cross-Site Request Forgery attacks by validating request origins
 * and sec-fetch-site headers. The request is allowed if either validation passes.
 *
 * @see {@link https://hono.dev/docs/middleware/builtin/csrf}
 *
 * @param {CSRFOptions} [options] - The options for the CSRF protection middleware.
 * @param {string|string[]|(origin: string, context: Context) => boolean} [options.origin] -
 *   Allowed origins for requests.