72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
import { Logger } from '@nestjs/common';
|
|
import { identityApp, type IdentityApp } from './helpers/identity-app';
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
describe('HTTP security boundaries', () => {
|
|
let ctx: IdentityApp;
|
|
beforeAll(async () => {
|
|
ctx = await identityApp();
|
|
}, 60000);
|
|
afterAll(async () => {
|
|
await ctx?.close();
|
|
});
|
|
afterEach(() => jest.restoreAllMocks());
|
|
it('bounds bodies and rejects malformed JSON with distinct codes', async () => {
|
|
const large = await ctx
|
|
.api()
|
|
.post('/api/v1/products')
|
|
.auth(ctx.token, { type: 'bearer' })
|
|
.send({ name: 'A', slug: 'a', description: 'x'.repeat(40000) })
|
|
.expect(413);
|
|
expect(large.body.code).toBe('REQUEST_TOO_LARGE');
|
|
const invalid = await ctx
|
|
.api()
|
|
.post('/api/v1/products')
|
|
.set('Content-Type', 'application/json')
|
|
.send('{"invalid":')
|
|
.expect(400);
|
|
expect(invalid.body.code).toBe('REQUEST_MALFORMED');
|
|
});
|
|
it('generates its own request IDs and ignores cookie credentials', async () => {
|
|
const response = await ctx
|
|
.api()
|
|
.get('/api/v1/addresses')
|
|
.set('X-Request-Id', 'attacker-controlled')
|
|
.set('Cookie', 'accessToken=' + ctx.token)
|
|
.expect(401);
|
|
expect(response.body.requestId).toMatch(/^[a-f0-9-]{36}$/);
|
|
expect(response.headers['x-request-id']).toBe(response.body.requestId);
|
|
expect(response.headers['cache-control']).toBe('no-store');
|
|
});
|
|
it('treats SQL-looking input as data and prevents field injection', async () => {
|
|
const name = "Robert'); DROP TABLE users;--";
|
|
await ctx
|
|
.api()
|
|
.post('/api/v1/products')
|
|
.auth(ctx.token, { type: 'bearer' })
|
|
.send({ name, slug: randomUUID() })
|
|
.expect(201);
|
|
expect(await ctx.db.user.count()).toBeGreaterThan(0);
|
|
await ctx
|
|
.api()
|
|
.get('/api/v1/products?limit=999999')
|
|
.auth(ctx.token, { type: 'bearer' })
|
|
.expect(400);
|
|
});
|
|
it('logs an unexpected database failure without exposing sensitive text', async () => {
|
|
const logger = jest.spyOn(Logger.prototype, 'error').mockImplementation();
|
|
jest
|
|
.spyOn(ctx.db.product, 'findMany')
|
|
.mockRejectedValueOnce(new Error('SELECT secret password=SECRET'));
|
|
const response = await ctx
|
|
.api()
|
|
.get('/api/v1/products')
|
|
.auth(ctx.token, { type: 'bearer' })
|
|
.expect(500);
|
|
expect(response.body.code).toBe('INTERNAL_FAILURE');
|
|
expect(JSON.stringify([response.body, logger.mock.calls])).not.toContain(
|
|
'SECRET',
|
|
);
|
|
});
|
|
});
|