c5a2fc9105
- safeReturnTo resolves the target against a placeholder origin and rejects backslashes and control characters, so `/\evil.example` and similar payloads can no longer redirect off-site after sign-in. - Add a real unauthenticated /api/health and answer unknown /api/* paths with a JSON 404 instead of the SPA shell. - Limit request bodies: 16 KB for /api/auth/* (read before a session exists), 128 MB for resource create/update and uploads, 10 MB elsewhere. - Run the production image as an unprivileged user and add a Docker HEALTHCHECK against /api/health.
49 lines
1.9 KiB
TypeScript
49 lines
1.9 KiB
TypeScript
import {describe, expect, test} from 'bun:test'
|
|
import server from './index'
|
|
|
|
describe('API surface hardening', () => {
|
|
test('serves an unauthenticated JSON health check', async () => {
|
|
const res = await server.fetch(new Request('http://localhost/api/health'))
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(await res.json()).toEqual({status: 'ok'})
|
|
})
|
|
|
|
test('answers unknown API paths with JSON instead of the SPA shell', async () => {
|
|
const res = await server.fetch(new Request('http://localhost/api/does-not-exist'))
|
|
|
|
expect(res.headers.get('content-type')).toContain('application/json')
|
|
})
|
|
|
|
test('rejects an oversized sign-in body before parsing it', async () => {
|
|
const res = await server.fetch(new Request('http://localhost/api/auth/login', {
|
|
method: 'POST',
|
|
headers: {'content-type': 'application/x-www-form-urlencoded', accept: 'application/json'},
|
|
body: `username=admin&password=${'x'.repeat(20 * 1024)}`,
|
|
}))
|
|
|
|
expect(res.status).toBe(413)
|
|
expect(await res.json()).toEqual({error: 'Request body is too large'})
|
|
})
|
|
|
|
test('keeps the upload ceiling for object uploads', async () => {
|
|
const res = await server.fetch(new Request('http://localhost/api/clouds/aws/services/storage/resources/bucket-1/object?key=big.bin', {
|
|
method: 'PUT',
|
|
headers: {'content-type': 'application/octet-stream'},
|
|
body: new Uint8Array(11 * 1024 * 1024),
|
|
}))
|
|
|
|
expect(res.status).toBe(401)
|
|
})
|
|
|
|
test('applies the default limit to other API routes', async () => {
|
|
const res = await server.fetch(new Request('http://localhost/api/clouds/aws/services/messaging/resources/queue-1/messages', {
|
|
method: 'POST',
|
|
headers: {'content-type': 'application/json'},
|
|
body: new Uint8Array(11 * 1024 * 1024),
|
|
}))
|
|
|
|
expect(res.status).toBe(413)
|
|
})
|
|
})
|