Files
floci-ui/packages/api/src/index.hardening.test.ts
Zakaria c5a2fc9105 fix(api): block open redirects, add health check and body limits, run as non-root
- 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.
2026-10-23 10:43:41 -04:00

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)
})
})