π Technical Security Controls
π― Implementing defense-in-depth for API services
π Document Owner: CEO | π Version: 1.0 | π
Last Updated: 2025-02-16 (UTC)
π Review Cycle: Quarterly | β° Next Review: 2025-05-16
This document explains the security headers implemented to protect the European Parliament MCP Server API, in compliance with Hack23 AB's ISMS Secure Development Policy.
As a Node.js/TypeScript API server implementing the Model Context Protocol (MCP), this service requires comprehensive HTTP security headers to protect against common web vulnerabilities and ensure secure communication with MCP clients accessing European Parliament data.
This implementation aligns with:
HTTP Header:
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'
Purpose: Restricts the sources from which content can be loaded. For an API server, we use a highly restrictive policy.
Directives Explained:
default-src 'none' - Deny all resource loading by default (API doesn't serve client-side content)frame-ancestors 'none' - Prevent API responses from being embedded in frames (clickjacking protection)base-uri 'none' - Prevent base tag injection attacksImplementation (Express.js):
import helmet from 'helmet';
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'none'"]
}
}));
ZAP/OWASP Issues Addressed:
HTTP Header:
X-Frame-Options: DENY
Purpose: Prevents API responses from being displayed in frames, iframes, or embedded objects.
Implementation (Express.js):
app.use(helmet.frameguard({ action: 'deny' }));
ZAP/OWASP Issues Addressed:
HTTP Header:
X-Content-Type-Options: nosniff
Purpose: Prevents browsers from MIME-sniffing responses, ensuring the declared Content-Type is respected.
Implementation (Express.js):
app.use(helmet.noSniff());
ZAP/OWASP Issues Addressed:
HTTP Header:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Purpose: Forces all connections to use HTTPS, preventing protocol downgrade attacks.
Configuration:
max-age=31536000 - 1 year validityincludeSubDomains - Apply to all subdomainspreload - Eligible for browser HSTS preload listsImplementation (Express.js):
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
ZAP/OWASP Issues Addressed:
Note: Only enable HSTS if your API is served exclusively over HTTPS.
HTTP Header:
X-XSS-Protection: 0
Purpose: Disables legacy XSS filters that can introduce vulnerabilities. Modern protection comes from CSP.
Implementation (Express.js):
app.use(helmet.xssFilter());
Rationale: Modern browsers have deprecated XSS filters due to security issues. CSP provides better protection.
HTTP Header:
Referrer-Policy: no-referrer
Purpose: Controls referrer information sent with requests. For API security, no referrer is sent.
Implementation (Express.js):
app.use(helmet.referrerPolicy({ policy: 'no-referrer' }));
Benefits:
HTTP Header:
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()
Purpose: Controls which browser features and APIs can be used.
Implementation (Express.js):
app.use(helmet.permissionsPolicy({
features: {
geolocation: [],
microphone: [],
camera: [],
payment: [],
usb: []
}
}));
Features Disabled:
HTTP Header:
Cross-Origin-Opener-Policy: same-origin
Purpose: Isolates the browsing context from cross-origin documents.
Implementation (Express.js):
app.use(helmet.crossOriginOpenerPolicy({ policy: 'same-origin' }));
ZAP/OWASP Issues Addressed:
HTTP Header:
Cross-Origin-Resource-Policy: same-origin
Purpose: Prevents other origins from reading the resource.
Implementation (Express.js):
app.use(helmet.crossOriginResourcePolicy({ policy: 'same-origin' }));
Benefits:
HTTP Header:
Cross-Origin-Embedder-Policy: require-corp
Purpose: Prevents loading cross-origin resources without explicit permission.
Implementation (Express.js):
app.use(helmet.crossOriginEmbedderPolicy());
HTTP Header:
X-API-Version: 1.0.0
Purpose: Informs clients of the API version for compatibility and debugging.
Implementation (Express.js):
app.use((req, res, next) => {
res.setHeader('X-API-Version', '1.0.0');
next();
});
HTTP Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1644841200
Purpose: Informs clients of rate limiting status to prevent abuse.
Implementation (Express.js):
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false
});
app.use('/api/', limiter);
Benefits:
HTTP Header:
Cache-Control: no-store, no-cache, must-revalidate, private
Purpose: Prevents caching of sensitive European Parliament data.
Implementation (Express.js):
app.use((req, res, next) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
GDPR Alignment:
HTTP Headers:
Access-Control-Allow-Origin: https://trusted-mcp-client.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
Implementation (Express.js):
import cors from 'cors';
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
Security Considerations:
Access-Control-Allow-Origin: * for authenticated APIsimport express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
const app = express();
// 1. Helmet - Core security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'none'"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
},
referrerPolicy: {
policy: 'no-referrer'
},
permissionsPolicy: {
features: {
geolocation: [],
microphone: [],
camera: [],
payment: [],
usb: []
}
}
}));
// 2. CORS configuration
const corsOptions = {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
methods: ['GET', 'POST', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
};
app.use(cors(corsOptions));
// 3. Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', limiter);
// 4. Custom security headers
app.use((req, res, next) => {
res.setHeader('X-API-Version', '1.0.0');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
// Your API routes here
app.use('/api', apiRoutes);
export default app;
Create comprehensive tests to validate all security headers:
import request from 'supertest';
import app from './app';
describe('Security Headers', () => {
it('should set CSP header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['content-security-policy']).toContain("default-src 'none'");
});
it('should set X-Frame-Options header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['x-frame-options']).toBe('DENY');
});
it('should set X-Content-Type-Options header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['x-content-type-options']).toBe('nosniff');
});
it('should set Strict-Transport-Security header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['strict-transport-security']).toContain('max-age=31536000');
});
it('should set Referrer-Policy header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['referrer-policy']).toBe('no-referrer');
});
it('should set COOP header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['cross-origin-opener-policy']).toBe('same-origin');
});
it('should set CORP header', async () => {
const response = await request(app).get('/api/health');
expect(response.headers['cross-origin-resource-policy']).toBe('same-origin');
});
it('should set Cache-Control for API responses', async () => {
const response = await request(app).get('/api/parliament/members');
expect(response.headers['cache-control']).toContain('no-store');
});
it('should enforce rate limiting', async () => {
const requests = Array(101).fill(null).map(() => request(app).get('/api/health'));
const responses = await Promise.all(requests);
const tooManyRequests = responses.filter(r => r.status === 429);
expect(tooManyRequests.length).toBeGreaterThan(0);
});
});
# Start the API server
npm start
# Run ZAP baseline scan
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t http://localhost:3000/api \
-r zap-report.html
# Run ZAP full scan (for comprehensive testing)
docker run -t owasp/zap2docker-stable zap-full-scan.py \
-t http://localhost:3000/api \
-r zap-full-report.html
For European Parliament data handling, consider additional headers:
// Indicate data processing purpose
res.setHeader('X-Data-Processing-Purpose', 'european-parliament-api-access');
// Indicate data retention policy
res.setHeader('X-Data-Retention-Days', '90');
// Indicate GDPR compliance
res.setHeader('X-GDPR-Compliant', 'true');
Note: These are custom headers for internal documentation and should not replace proper GDPR compliance measures.
Implement logging for security header violations:
app.use((req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
// Log if security headers are missing
const requiredHeaders = [
'content-security-policy',
'x-frame-options',
'x-content-type-options',
'strict-transport-security'
];
requiredHeaders.forEach(header => {
if (!res.getHeader(header)) {
console.error(`Security header missing: ${header}`, {
path: req.path,
method: req.method,
timestamp: new Date().toISOString()
});
}
});
return originalSend.call(this, data);
};
next();
});
HTTP security headers are supported by:
Before deploying to production:
*)If using a reverse proxy (nginx, Cloudflare, etc.), configure headers at that level:
Nginx example:
add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none';" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Referrer-Policy "no-referrer" always;
π Document Control:
β
Approved by: James Pether SΓΆrling, CEO
π€ Distribution: Public
π·οΈ Classification:
π
Effective Date: 2025-02-16
β° Next Review: 2025-05-16
π― Framework Compliance: