Test CORS headers for the wildcard+credentials misconfiguration, verify rate limiting effectiveness, and validate all security response headers in NestJS APIs.
CORS misconfiguration is one of the most common API security issues. A misconfigured Access-Control-Allow-Origin header allows a malicious website to make authenticated cross-origin requests using the victim's cookies. Combined with missing rate limiting, this enables automated attacks bypassing all other security controls.
This lesson covers systematic testing of CORS policy, rate limiting, and the full set of required security response headers.
The most dangerous CORS misconfiguration: Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true.
1describe('CORS Security', () => {
2 it('does not echo arbitrary Origin header', async () => {
3 const res = await request(app.getHttpServer())
4 .get('/api/courses')
5 .set('Origin', 'https://evil-attacker.com');
6
7 const allowedOrigin = res.headers['access-control-allow-origin'];
8 expect(allowedOrigin).not.toBe('https://evil-attacker.com');
9 if (res.headers['access-control-allow-credentials'] === 'true') {
10 expect(allowedOrigin).not.toBe('*');
11 }
12 });
13
14 it('null origin is not allowed (file:// CORS attacks)', async () => {
15 const res = await request(app.getHttpServer())
16 .get('/api/courses')
17 .set('Origin', 'null');
18 expect(res.headers['access-control-allow-origin']).not.toBe('null');
19 });
20
21 it('wildcard + credentials is forbidden (critical misconfiguration)', async () => {
22 const res = await request(app.getHttpServer()).get('/api/courses');
23 const acao = res.headers['access-control-allow-origin'];
24 const acac = res.headers['access-control-allow-credentials'];
25 if (acao === '*') {
26 expect(acac).not.toBe('true');
27 }
28 });
29
30 it('OPTIONS preflight returns correct headers', async () => {
31 const res = await request(app.getHttpServer())
32 .options('/api/courses')
33 .set('Origin', 'http://localhost:3000')
34 .set('Access-Control-Request-Method', 'GET')
35 .set('Access-Control-Request-Headers', 'Authorization');
36 expect(res.status).toBe(204);
37 expect(res.headers['access-control-allow-methods']).toContain('GET');
38 });
39});
40
41// NestJS correct CORS configuration:
42// const ALLOWED_ORIGINS = [
43// 'https://developerbootstrap.com',
44// process.env.NODE_ENV !== 'production' ? 'http://localhost:3000' : null,
45// ].filter(Boolean) as string[];
46//
47// app.enableCors({
48// origin: (origin, callback) => {
49// if (!origin || ALLOWED_ORIGINS.includes(origin)) callback(null, origin);
50// else callback(new Error('Not allowed by CORS'));
51// },
52// credentials: true,
53// });1describe('Rate Limiting', () => {
2 it('auth endpoint blocks after 10 rapid failures', async () => {
3 const responses = await Promise.all(
4 Array.from({ length: 15 }, () =>
5 request(app.getHttpServer())
6 .post('/auth/login')
7 .send({ email: '[email protected]', password: 'wrong' })
8 )
9 );
10 const limited = responses.filter(r => r.status === 429);
11 expect(limited.length).toBeGreaterThan(0);
12 });
13
14 it('returns Retry-After header on 429', async () => {
15 // Send enough requests to trigger rate limit
16 await Promise.all(
17 Array.from({ length: 20 }, () =>
18 request(app.getHttpServer())
19 .get('/api/courses')
20 .set('Authorization', 'Bearer ' + userToken)
21 )
22 );
23 const res = await request(app.getHttpServer())
24 .get('/api/courses')
25 .set('Authorization', 'Bearer ' + userToken);
26
27 if (res.status === 429) {
28 expect(res.headers['retry-after']).toBeDefined();
29 expect(parseInt(res.headers['retry-after'])).toBeGreaterThan(0);
30 }
31 });
32
33 it('rate limit headers are present on successful responses', async () => {
34 const res = await request(app.getHttpServer())
35 .get('/api/courses')
36 .set('Authorization', 'Bearer ' + userToken);
37
38 const hasLimit = res.headers['x-ratelimit-limit'] ||
39 res.headers['ratelimit-limit'];
40 expect(hasLimit).toBeDefined();
41 });
42});Unbounded request sizes enable DoS attacks.
1describe('Request Size Limits', () => {
2 it('rejects requests with body > 1MB', async () => {
3 const largeBody = JSON.stringify({ data: 'A'.repeat(1024 * 1024 + 1) });
4 const res = await request(app.getHttpServer())
5 .post('/api/courses')
6 .set('Authorization', 'Bearer ' + adminToken)
7 .set('Content-Type', 'application/json')
8 .send(largeBody);
9 expect([400, 413]).toContain(res.status);
10 });
11
12 it('validates Content-Type on POST endpoints', async () => {
13 const res = await request(app.getHttpServer())
14 .post('/auth/login')
15 .set('Content-Type', 'text/plain')
16 .send('email=test&password=test');
17 expect([400, 415]).toContain(res.status);
18 });
19});
20
21// NestJS size limit configuration:
22// import { json, urlencoded } from 'express';
23// app.use(json({ limit: '1mb' }));
24// app.use(urlencoded({ extended: true, limit: '1mb' }));1describe('Security Response Headers', () => {
2 let res: Response;
3
4 beforeAll(async () => {
5 res = await request(app.getHttpServer()).get('/api/health');
6 });
7
8 it('X-Content-Type-Options: nosniff', () => {
9 expect(res.headers['x-content-type-options']).toBe('nosniff');
10 });
11
12 it('X-Frame-Options: DENY', () => {
13 expect(res.headers['x-frame-options']).toBe('DENY');
14 });
15
16 it('Referrer-Policy restricts referrer information', () => {
17 expect(res.headers['referrer-policy']).toMatch(/strict-origin|no-referrer/);
18 });
19
20 it('Permissions-Policy restricts dangerous features', () => {
21 const pp = res.headers['permissions-policy'];
22 expect(pp).toBeDefined();
23 expect(pp).toContain('camera=()');
24 expect(pp).toContain('microphone=()');
25 });
26
27 it('does not expose server technology', () => {
28 expect(res.headers['x-powered-by']).toBeUndefined();
29 expect(res.headers['server']).toBeUndefined();
30 });
31
32 it('Content-Security-Policy is present', () => {
33 expect(res.headers['content-security-policy']).toBeDefined();
34 expect(res.headers['content-security-policy']).toContain("default-src 'self'");
35 });
36});A CLI script to verify security headers against a staging URL.
1// scripts/check-security-headers.cjs
2const https = require('https');
3const http = require('http');
4
5async function checkHeaders(url) {
6 const parsed = new URL(url);
7 const client = parsed.protocol === 'https:' ? https : http;
8 const headers = await new Promise((resolve, reject) => {
9 client.get(url + '/api/health', (res) => {
10 resolve(res.headers);
11 res.resume();
12 }).on('error', reject);
13 });
14
15 const checks = [
16 { name: 'x-content-type-options', expected: 'nosniff', required: true },
17 { name: 'x-frame-options', expected: 'DENY', required: true },
18 { name: 'referrer-policy', required: true },
19 { name: 'permissions-policy', required: true },
20 { name: 'content-security-policy', required: true },
21 ];
22
23 let failed = 0;
24 for (const check of checks) {
25 const value = headers[check.name];
26 const pass = value && (!check.expected || value === check.expected);
27 console.log((pass ? 'PASS' : 'FAIL') + ' ' + check.name + ': ' + (value || 'absent'));
28 if (!pass && check.required) failed++;
29 }
30
31 if (failed > 0) {
32 console.error(failed + ' required security headers missing');
33 process.exit(1);
34 }
35}
36
37checkHeaders(process.argv[2] || 'http://localhost:3000');Comprehensive security headers and CORS test suite for a NestJS API covering all OWASP recommended headers and common CORS misconfiguration patterns.
1// apps/api/src/security/__tests__/headers-cors.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { INestApplication } from '@nestjs/common';
4import * as request from 'supertest';
5import { AppModule } from '../../app.module';
6
7describe('Security Headers and CORS', () => {
8 let app: INestApplication;
9
10 beforeAll(async () => {
11 const module: TestingModule = await Test.createTestingModule({
12 imports: [AppModule],
13 }).compile();
14 app = module.createNestApplication();
15 await app.init();
16 });
17
18 afterAll(() => app.close());
19
20 describe('Required security headers', () => {
21 it('sets all required headers on API responses', async () => {
22 const res = await request(app.getHttpServer()).get('/api/health');
23 expect(res.headers['x-content-type-options']).toBe('nosniff');
24 expect(res.headers['x-frame-options']).toBe('DENY');
25 expect(res.headers['referrer-policy']).toMatch(/strict-origin|no-referrer/);
26 expect(res.headers['x-powered-by']).toBeUndefined();
27 });
28
29 it('CSP present and blocks inline scripts', async () => {
30 const res = await request(app.getHttpServer()).get('/api/health');
31 const csp = res.headers['content-security-policy'];
32 expect(csp).toBeDefined();
33 expect(csp).toContain("default-src 'self'");
34 expect(csp).not.toMatch(/script-src.*'unsafe-inline'/);
35 });
36 });
37
38 describe('CORS policy', () => {
39 it('does not echo untrusted origin', async () => {
40 const res = await request(app.getHttpServer())
41 .get('/api/courses')
42 .set('Origin', 'https://malicious-site.com');
43 expect(res.headers['access-control-allow-origin']).not.toBe('https://malicious-site.com');
44 });
45
46 it('null origin is not allowed', async () => {
47 const res = await request(app.getHttpServer())
48 .get('/api/courses')
49 .set('Origin', 'null');
50 expect(res.headers['access-control-allow-origin']).not.toBe('null');
51 });
52
53 it('wildcard + credentials is forbidden', async () => {
54 const res = await request(app.getHttpServer()).get('/api/courses');
55 const acao = res.headers['access-control-allow-origin'];
56 const acac = res.headers['access-control-allow-credentials'];
57 if (acao === '*') {
58 expect(acac).not.toBe('true');
59 }
60 });
61
62 it('allows configured trusted origin', async () => {
63 const trustedOrigin = process.env['CORS_ORIGIN'] || 'http://localhost:3000';
64 const res = await request(app.getHttpServer())
65 .get('/api/courses')
66 .set('Origin', trustedOrigin);
67 expect(res.status).not.toBe(403);
68 });
69 });
70
71 describe('Request size limits', () => {
72 it('rejects oversized JSON body', async () => {
73 const largeBody = JSON.stringify({ data: 'X'.repeat(1024 * 1024 + 100) });
74 const res = await request(app.getHttpServer())
75 .post('/api/courses')
76 .set('Content-Type', 'application/json')
77 .send(largeBody);
78 expect([400, 413]).toContain(res.status);
79 });
80 });
81});This test suite covers security headers (verified on all API responses), CORS policy (null origin, untrusted origin, wildcard+credentials), and request size limits. The null origin test prevents file:// protocol CORS attacks. The wildcard+credentials test catches the most dangerous misconfiguration. Run in CI after every staging deployment to catch header regressions introduced by configuration changes.
The next lesson covers dependency vulnerability scanning with npm audit and Snyk — configuring SCA pipelines, interpreting CVSS scores, managing exceptions, and setting up Dependabot for automated updates.
Your progress is saved in this browser, on this device only.