Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
test: add more csrf unit tests, handle missing origin
  • Loading branch information
Varixo authored and wmertens committed Sep 21, 2025
commit 5b0a92050e7e053ba6bc33b2ebb5095ce673f70c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ function checkCSRF(requestEv: RequestEvent, laxProto?: true) {
if (isForm) {
const inputOrigin = requestEv.request.headers.get('origin');
const origin = requestEv.url.origin;

// Reject requests with missing origin headers for form submissions
if (!inputOrigin) {
throw requestEv.error(
403,
`CSRF check failed. Cross-site ${requestEv.method} form submissions are forbidden.
The request is missing the origin header.`
);
}

let forbidden = inputOrigin !== origin;

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('csrf handler', () => {
{
contentType: 'text/plain',
},
])('should throw an error if the origin does not match for $contentType', ({ contentType }) => {
])('should reject request when the origin does not match for $contentType', ({ contentType }) => {
const errorFn = vi.fn();
const requestEv = {
request: {
Expand All @@ -26,13 +26,131 @@ describe('csrf handler', () => {
error: errorFn,
} as unknown as RequestEvent;

expect(() => csrfCheckMiddleware(requestEv)).toThrow();
expect(errorFn).toBeCalledWith(403, expect.stringMatching('CSRF check failed'));
});

it('should reject request when origin header is missing for form content types', () => {
const errorFn = vi.fn();
const requestEv = {
request: {
headers: new Headers({
'content-type': 'application/x-www-form-urlencoded',
// No origin header
}),
},
url: new URL('http://example.com'),
error: errorFn,
} as unknown as RequestEvent;

expect(() => csrfCheckMiddleware(requestEv)).toThrow();
expect(errorFn).toBeCalledWith(403, expect.stringMatching('CSRF check failed'));
});

it.each([
{
contentType: 'application/x-www-form-urlencoded',
},
{
contentType: 'multipart/form-data',
},
{
contentType: 'text/plain',
},
])('should allow request when origin matches for $contentType', ({ contentType }) => {
const errorFn = vi.fn();
const requestEv = {
request: {
headers: new Headers({
'content-type': contentType,
origin: 'http://example.com',
}),
},
url: new URL('http://example.com'),
error: errorFn,
} as unknown as RequestEvent;

// Should not throw an error
expect(() => csrfCheckMiddleware(requestEv)).not.toThrow();
expect(errorFn).not.toHaveBeenCalled();
});

it.each([
{
contentType: 'application/json',
},
{
contentType: 'text/html',
},
{
contentType: 'application/xml',
},
{
contentType: 'image/png',
},
])(
'should allow request for non-form content type $contentType regardless of origin',
({ contentType }) => {
const errorFn = vi.fn();
const requestEv = {
request: {
headers: new Headers({
'content-type': contentType,
origin: 'http://example.com',
}),
},
url: new URL('http://bad-example.com'),
error: errorFn,
} as unknown as RequestEvent;

// Should not throw an error for non-form content types
expect(() => csrfCheckMiddleware(requestEv)).not.toThrow();
expect(errorFn).not.toHaveBeenCalled();
}
);

it('should allow request when content-type header is missing', () => {
const errorFn = vi.fn();
const requestEv = {
request: {
headers: new Headers({
origin: 'http://example.com',
// No content-type header
}),
},
url: new URL('http://example.com'),
error: errorFn,
} as unknown as RequestEvent;

// Should not throw an error when content-type is missing
expect(() => csrfCheckMiddleware(requestEv)).not.toThrow();
expect(errorFn).not.toHaveBeenCalled();
});

it('should verify exact error message content', () => {
const errorFn = vi.fn();
const requestEv = {
request: {
headers: new Headers({
'content-type': 'application/x-www-form-urlencoded',
origin: 'http://malicious.com',
}),
},
url: new URL('http://example.com'),
method: 'POST',
error: errorFn,
} as unknown as RequestEvent;

try {
csrfCheckMiddleware(requestEv);
} catch (_) {
// ignore the error here, we just want to check the errorFn
}

expect(errorFn).toBeCalledWith(403, expect.stringMatching('CSRF check failed'));
expect(errorFn).toBeCalledWith(
403,
'CSRF check failed. Cross-site POST form submissions are forbidden.\nThe request origin "http://malicious.com" does not match the server origin "http://example.com".'
);
});

describe('isContentType', () => {
Expand All @@ -43,5 +161,53 @@ describe('csrf handler', () => {
});
expect(isContentType(headers, 'multipart/form-data')).toBe(true);
});

it('should handle multiple content type parameters', () => {
const headers = new Headers({
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(true);
});

it('should handle case insensitive content types', () => {
const headers = new Headers({
'content-type': 'APPLICATION/X-WWW-FORM-URLENCODED',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(false);
});

it('should return false for non-matching content types', () => {
const headers = new Headers({
'content-type': 'application/json',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(false);
});

it('should handle empty content-type header', () => {
const headers = new Headers({});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(false);
});

it('should handle missing content-type header', () => {
const headers = new Headers({
'other-header': 'value',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(false);
});

it('should handle multiple content type checks', () => {
const headers = new Headers({
'content-type': 'text/plain',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded', 'text/plain')).toBe(true);
expect(isContentType(headers, 'application/json', 'multipart/form-data')).toBe(false);
});

it('should handle content type with only whitespace', () => {
const headers = new Headers({
'content-type': ' ',
});
expect(isContentType(headers, 'application/x-www-form-urlencoded')).toBe(false);
});
});
});