Skip to content

Commit aa655c0

Browse files
dvlin-devCodex
andauthored
fix(moryflow/server): whitelist headers for originless POST to Better Auth (#230)
Previous blacklist-based cookie stripping failed because reverse proxies (1panel/nginx) can inject arbitrary headers (cookies, Sec-Fetch-*) that trigger Better Auth's formCsrfMiddleware with forceValidate=true, which then rejects the request with "Missing or null Origin". Switch to whitelist approach: for POST requests without Origin (device or server-to-server calls that never come from a browser), build Better Auth request headers from scratch with only known-safe headers. This eliminates all proxy-injected interference regardless of what the proxy adds. Legitimate browser POSTs always include Origin, so they continue to use the full header passthrough path unchanged. Co-authored-by: Codex <[email protected]>
1 parent 3ede683 commit aa655c0

File tree

1 file changed

+49
-20
lines changed

1 file changed

+49
-20
lines changed

apps/moryflow/server/src/auth/auth.controller.ts

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,19 @@ export class AuthController {
5353
}
5454

5555
const auth = this.authService.getAuth();
56-
const stripHeaders = this.resolveStripHeaders(req);
57-
const response = await auth.handler(
58-
buildAuthRequest(req, {
59-
path: req.originalUrl,
60-
stripRequestHeaders: stripHeaders,
61-
}),
62-
);
56+
const authRequest = this.isOriginlessPost(req)
57+
? buildAuthRequest(req, {
58+
path: req.originalUrl,
59+
includeRequestHeaders: false,
60+
headers: this.buildDeviceSafeHeaders(req),
61+
})
62+
: buildAuthRequest(req, {
63+
path: req.originalUrl,
64+
stripRequestHeaders: shouldIgnoreBrowserContextForAuthRequest(req)
65+
? [...BROWSER_CONTEXT_HEADER_NAMES]
66+
: undefined,
67+
});
68+
const response = await auth.handler(authRequest);
6369
const tokenizedResponse = await this.buildTokenizedAuthResponse(
6470
req,
6571
response,
@@ -275,23 +281,46 @@ export class AuthController {
275281
};
276282
}
277283

284+
private isOriginlessPost(req: ExpressRequest): boolean {
285+
return req.method === 'POST' && !req.headers.origin;
286+
}
287+
278288
/**
279-
* 决定传递给 Better Auth 时需要清理的请求头
280-
*
281-
* - 设备端 token-first 请求:清理 origin/referer/cookie
282-
* - 无 Origin 的 POST 请求:清理 cookie(反代可能剥离 X-App-Platform 但注入自身 cookie,
283-
* 合法浏览器 POST 必定携带 Origin,因此无 Origin + cookie 不是合法浏览器上下文)
289+
* 无 Origin 的 POST 只转发已知安全的头给 Better Auth(白名单)
290+
* 合法浏览器 POST 必定携带 Origin;无 Origin 的 POST 来自设备端或反代后的
291+
* 服务端调用,不应携带 cookie / Sec-Fetch-* 等浏览器上下文头。
292+
* 反代(1panel/nginx)可能注入 cookie 或透传 Sec-Fetch-*,黑名单式 strip
293+
* 无法穷举;白名单方式从根本上杜绝干扰头进入 Better Auth CSRF 中间件。
284294
*/
285-
private resolveStripHeaders(req: ExpressRequest): string[] | undefined {
286-
if (shouldIgnoreBrowserContextForAuthRequest(req)) {
287-
return [...BROWSER_CONTEXT_HEADER_NAMES];
288-
}
289-
290-
if (req.method === 'POST' && !req.headers.origin) {
291-
return ['cookie'];
295+
private buildDeviceSafeHeaders(req: ExpressRequest): Headers {
296+
const headers = new Headers();
297+
const forwarded: readonly string[] = [
298+
'content-type',
299+
'accept',
300+
'user-agent',
301+
'accept-language',
302+
'x-app-platform',
303+
'x-forwarded-for',
304+
'x-forwarded-proto',
305+
'x-forwarded-host',
306+
'x-forwarded-port',
307+
'x-real-ip',
308+
'x-request-id',
309+
];
310+
311+
for (const name of forwarded) {
312+
const value = req.headers[name];
313+
if (typeof value === 'string' && value.trim()) {
314+
headers.set(name, value);
315+
} else if (Array.isArray(value)) {
316+
const joined = value.join(', ');
317+
if (joined.trim()) {
318+
headers.set(name, joined);
319+
}
320+
}
292321
}
293322

294-
return undefined;
323+
return headers;
295324
}
296325

297326
private readBodyString(req: ExpressRequest, key: string): string | undefined {

0 commit comments

Comments
 (0)