fix(security): block state-changing requests with missing Origin header
- commit
- 00a75bb372cd4f5f3b187f74fc5a8fc3b55bbcbc
- author
- Kit · · 2026-02-16 11:02 UTC
- parent
- c31e89e
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
1 file · +20 -13
M src/hooks.server.ts +20 -13
| @@ -86,21 +86,28 @@ export const handle: Handle = async ({ event, resolve }) => { | |||
| 86 | 86 | const origin = event.request.headers.get('origin'); | |
| 87 | 87 | const host = event.request.headers.get('host'); | |
| 88 | 88 | ||
| 89 | - | // Block cross-origin requests (origin header is present but doesn't match host) | |
| 90 | - | if (origin) { | |
| 91 | - | try { | |
| 92 | - | const originUrl = new URL(origin); | |
| 93 | - | if (originUrl.host !== host) { | |
| 94 | - | log.warn('CSRF check failed - origin mismatch', { | |
| 95 | - | origin: originUrl.host, | |
| 96 | - | host | |
| 97 | - | }); | |
| 98 | - | return new Response('Forbidden', { status: 403 }); | |
| 99 | - | } | |
| 100 | - | } catch { | |
| 101 | - | log.warn('CSRF check failed - invalid origin', { origin }); | |
| 89 | + | if (!origin) { | |
| 90 | + | // Browsers always send Origin on state-changing requests. | |
| 91 | + | // Missing Origin means non-browser client (curl, scripts). | |
| 92 | + | log.warn('CSRF check failed - missing origin header', { | |
| 93 | + | method: event.request.method, | |
| 94 | + | path: event.url.pathname | |
| 95 | + | }); | |
| 96 | + | return new Response('Forbidden', { status: 403 }); | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | try { | |
| 100 | + | const originUrl = new URL(origin); | |
| 101 | + | if (originUrl.host !== host) { | |
| 102 | + | log.warn('CSRF check failed - origin mismatch', { | |
| 103 | + | origin: originUrl.host, | |
| 104 | + | host | |
| 105 | + | }); | |
| 102 | 106 | return new Response('Forbidden', { status: 403 }); | |
| 103 | 107 | } | |
| 108 | + | } catch { | |
| 109 | + | log.warn('CSRF check failed - invalid origin', { origin }); | |
| 110 | + | return new Response('Forbidden', { status: 403 }); | |
| 104 | 111 | } | |
| 105 | 112 | } | |
| 106 | 113 | ||
| @@ -86,21 +86,28 @@ export const handle: Handle = async ({ event, resolve }) => { | |||||
| 86 | const origin = event.request.headers.get('origin'); | 86 | const origin = event.request.headers.get('origin'); | ||
| 87 | const host = event.request.headers.get('host'); | 87 | const host = event.request.headers.get('host'); | ||
| 88 | 88 | ||||
| 89 | - | // Block cross-origin requests (origin header is present but doesn't match host) | 89 | + | if (!origin) { |
| 90 | - | if (origin) { | 90 | + | // Browsers always send Origin on state-changing requests. |
| 91 | - | try { | 91 | + | // Missing Origin means non-browser client (curl, scripts). |
| 92 | - | const originUrl = new URL(origin); | 92 | + | log.warn('CSRF check failed - missing origin header', { |
| 93 | - | if (originUrl.host !== host) { | 93 | + | method: event.request.method, |
| 94 | - | log.warn('CSRF check failed - origin mismatch', { | 94 | + | path: event.url.pathname |
| 95 | - | origin: originUrl.host, | 95 | + | }); |
| 96 | - | host | 96 | + | return new Response('Forbidden', { status: 403 }); |
| 97 | - | }); | 97 | + | } |
| 98 | - | return new Response('Forbidden', { status: 403 }); | 98 | + | |
| 99 | - | } | 99 | + | try { |
| 100 | - | } catch { | 100 | + | const originUrl = new URL(origin); |
| 101 | - | log.warn('CSRF check failed - invalid origin', { origin }); | 101 | + | if (originUrl.host !== host) { |
| 102 | + | log.warn('CSRF check failed - origin mismatch', { | |||
| 103 | + | origin: originUrl.host, | |||
| 104 | + | host | |||
| 105 | + | }); | |||
| 102 | return new Response('Forbidden', { status: 403 }); | 106 | return new Response('Forbidden', { status: 403 }); | ||
| 103 | } | 107 | } | ||
| 108 | + | } catch { | |||
| 109 | + | log.warn('CSRF check failed - invalid origin', { origin }); | |||
| 110 | + | return new Response('Forbidden', { status: 403 }); | |||
| 104 | } | 111 | } | ||
| 105 | } | 112 | } | ||
| 106 | 113 | ||||