Astro 排除部分路徑不檢查 CSRF Origin

程式設計 # Astro

Astro 預設會檢查提交表單的 Origin 是否與網站的 Origin 相同,目的是為了防止 CSRF 攻擊,但是在串第三方金流 API 的時候回傳的請求就會被擋下來,這回就來解決這個問題~

修改 Astro 的 originCheck Middleware

既然是 Astro 會擋下來,那我們就先關掉吧:

astro.config.ts
export default defineConfig({
security: {
// Change built-in origin check to custom function
// @see src/middleware.ts
checkOrigin: false,
},
})

然後在 src/middleware/originCheck.ts 新增一個 originCheck 的 middleware,裡面只新增了 EXCLUDE_PATHS 陣列,裡面放入需要排除的路徑,這樣就可以讓 Astro 在這些路徑上不檢查 CSRF 的 Origin 了。比如我們這邊就放了金流的回傳請求路徑 /checkout/done

src/middleware/originCheck.ts
/**
* @reference https://github.com/withastro/astro/blob/d2d04b02773f82103db75076fcdd1f089503770a/packages/astro/src/core/app/middlewares.ts
*/
import { defineMiddleware } from 'astro:middleware'
/**
* Content types that can be passed when sending a request via a form
*
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/enctype
* @private
*/
const FORM_CONTENT_TYPES = [
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain',
]
// Note: TRACE is unsupported by undici/Node.js
const SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
// Paths that should be excluded from origin check
const EXCLUDE_PATHS = ['/checkout/done']
/**
* Returns a middleware function in charge to check the `origin` header.
*/
export const originCheck = defineMiddleware((context, next) => {
const { request, url, isPrerendered } = context
// Prerendered pages should be excluded
if (isPrerendered) {
return next()
}
// Safe methods don't require origin check
if (SAFE_METHODS.includes(request.method)) {
return next()
}
// Exclude specific paths from origin check
if (EXCLUDE_PATHS.includes(url.pathname)) {
return next()
}
const isSameOrigin = request.headers.get('origin') === url.origin
const hasContentType = request.headers.has('content-type')
if (hasContentType) {
const formLikeHeader = hasFormLikeHeader(request.headers.get('content-type'))
if (formLikeHeader && !isSameOrigin) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
status: 403,
})
}
} else {
if (!isSameOrigin) {
return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
status: 403,
})
}
}
return next()
})
function hasFormLikeHeader(contentType: string | null): boolean {
if (contentType) {
for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
return true
}
}
}
return false
}

最後在 src/middleware/index.ts 中套用這個 middleware:

src/middleware/index.ts
import { sequence } from 'astro:middleware'
import { originCheck } from './originCheck'
export const onRequest = sequence(
// 必須將 `originCheck` 放在 middleware 的第一個
originCheck,
...
)