remora/web/server/security.test.mjs

148 lines
3.3 KiB
JavaScript
Raw Normal View History

import assert from 'node:assert/strict'
import { Readable } from 'node:stream'
import { test } from 'node:test'
import { validBrowserContext, validUnsafeRequest } from './oauth.mjs'
import { ApiProxy } from './proxy.mjs'
import {
SESSION_COOKIE,
expiredSessionCookie,
sessionCookie,
sessionIdentifier,
} from './sessions.mjs'
const config = {
publicOrigin: 'https://remora.example.com',
secureCookie: true,
}
test('session cookies are host-only, opaque, secure, and not script-readable', () => {
const cookie = sessionCookie('opaque-session', config)
assert.equal(
cookie,
`${SESSION_COOKIE}=opaque-session; Path=/; HttpOnly; Secure; SameSite=Strict`,
)
assert.doesNotMatch(cookie, /Domain=/)
assert.match(expiredSessionCookie(config), /Max-Age=0/)
assert.equal(
sessionIdentifier({
headers: { cookie: `unrelated=1; ${SESSION_COOKIE}=opaque-session` },
}),
'opaque-session',
)
})
test('unsafe requests require both same-origin context and the CSRF value', () => {
assert.equal(
validUnsafeRequest(
{
headers: {
origin: config.publicOrigin,
'sec-fetch-site': 'same-origin',
'x-remora-csrf': 'csrf-value',
},
},
config,
'csrf-value',
),
true,
)
assert.equal(
validUnsafeRequest(
{
headers: {
origin: 'https://attacker.invalid',
'sec-fetch-site': 'cross-site',
'x-remora-csrf': 'csrf-value',
},
},
config,
'csrf-value',
),
false,
)
assert.equal(
validUnsafeRequest(
{
headers: {
origin: config.publicOrigin,
'sec-fetch-site': 'same-origin',
'x-remora-csrf': 'wrong',
},
},
config,
'csrf-value',
),
false,
)
})
test('browser WebSocket context must be same-origin', () => {
assert.equal(
validBrowserContext(
{
headers: {
origin: config.publicOrigin,
'sec-fetch-site': 'same-origin',
},
},
config,
),
true,
)
assert.equal(
validBrowserContext(
{
headers: {
origin: 'https://attacker.invalid',
'sec-fetch-site': 'cross-site',
},
},
config,
),
false,
)
})
test('event ticket requests use the validated public origin upstream', async () => {
let upstreamOptions
const proxy = new ApiProxy(
{
...config,
apiUpstream: new URL('https://api.remora.invalid/v1/'),
eventsUpstream: new URL('wss://api.remora.invalid/v1/events'),
},
{
authenticated: async () => ({
session: {
accessToken: 'server-token',
csrfToken: 'csrf-value',
},
}),
},
async (_url, options) => {
upstreamOptions = options
return new Response('{}', {
status: 201,
headers: { 'Content-Type': 'application/json' },
})
},
)
const request = Readable.from([])
request.method = 'POST'
request.headers = {
origin: config.publicOrigin,
'sec-fetch-site': 'same-origin',
'x-remora-csrf': 'csrf-value',
}
const response = {
writeHead: () => undefined,
end: () => undefined,
}
await proxy.route(
request,
response,
new URL(`${config.publicOrigin}/bff/api/event-tickets`),
)
assert.equal(upstreamOptions.headers.Origin, config.publicOrigin)
})