fix: handle skipOriginCheck array (#8582)

This commit is contained in:
Joél Solano
2026-03-13 23:46:35 -07:00
committed by Alex Yang
parent 6be0f95991
commit 331c4c4136
2 changed files with 82 additions and 16 deletions
@@ -537,6 +537,61 @@ describe("origin check middleware", async () => {
);
expect(sampleInternalEndpointInvalid.error?.status).toBe(403);
});
it("should skip origin check for matched paths when skipOriginCheck is set to an array", async () => {
const { client } = await getTestInstance({
trustedOrigins: ["https://trusted-site.com"],
advanced: {
disableOriginCheck: false,
},
plugins: [
{
id: "test",
init() {
return {
context: {
skipOriginCheck: ["/public/data"],
},
};
},
endpoints: {
publicEndpoint: createAuthEndpoint(
"/public/data",
{
method: "GET",
query: z.object({
callbackURL: z.string(),
}),
use: [originCheck((c) => c.query.callbackURL)],
},
async (c) => c.query.callbackURL,
),
protectedEndpoint: createAuthEndpoint(
"/protected/data",
{
method: "GET",
query: z.object({
callbackURL: z.string(),
}),
use: [originCheck((c) => c.query.callbackURL)],
},
async (c) => c.query.callbackURL,
),
},
},
],
});
const skipped = await client.$fetch(
"/public/data?callbackURL=https://malicious.com",
);
expect(skipped.data).toBe("https://malicious.com");
const blocked = await client.$fetch(
"/protected/data?callbackURL=https://malicious.com",
);
expect(blocked.error?.status).toBe(403);
});
});
describe("trusted origins with baseURL inferred from request", async () => {
@@ -18,6 +18,29 @@ function shouldSkipCSRFForBackwardCompat(ctx: GenericEndpointContext): boolean {
);
}
/**
* Checks if the origin check should be skipped for the current request.
* Handles both boolean (skip all) and array (skip specific paths) configurations.
*/
function shouldSkipOriginCheck(ctx: GenericEndpointContext): boolean {
const skipOriginCheck = ctx.context.skipOriginCheck;
if (skipOriginCheck === true) {
return true;
}
if (Array.isArray(skipOriginCheck) && ctx.request) {
try {
const basePath = new URL(ctx.context.baseURL).pathname;
const currentPath = normalizePathname(ctx.request.url, basePath);
return skipOriginCheck.some((skipPath) =>
currentPath.startsWith(skipPath),
);
} catch {
//
}
}
return false;
}
/**
* Logs deprecation warning for users relying on coupled behavior.
* Only logs if user explicitly set disableOriginCheck (not test environment default).
@@ -45,7 +68,7 @@ export const originCheckMiddleware = createAuthMiddleware(async (ctx) => {
}
await validateOrigin(ctx);
if (ctx.context.skipOriginCheck) {
if (shouldSkipOriginCheck(ctx)) {
return;
}
@@ -117,7 +140,7 @@ export const originCheck = (
if (!ctx.request) {
return;
}
if (ctx.context.skipOriginCheck) {
if (shouldSkipOriginCheck(ctx)) {
return;
}
const callbackURL = getValue(ctx);
@@ -199,20 +222,8 @@ async function validateOrigin(
return;
}
const skipOriginCheck = ctx.context.skipOriginCheck;
if (Array.isArray(skipOriginCheck)) {
try {
const basePath = new URL(ctx.context.baseURL).pathname;
const currentPath = normalizePathname(ctx.request.url, basePath);
const shouldSkipPath = skipOriginCheck.some((skipPath) =>
currentPath.startsWith(skipPath),
);
if (shouldSkipPath) {
return;
}
} catch {
// If parsing fails, don't skip - continue with validation
}
if (shouldSkipOriginCheck(ctx)) {
return;
}
const shouldValidate = forceValidate || useCookies;