| name | security-by-design |
| description | API security, MCP tool security, input validation, rate limiting, audit logging, secure authentication, and defense-in-depth principles |
| license | MIT |
Security By Design Skill
Context
This skill applies when:
- Implementing MCP protocol tools and handlers
- Exposing European Parliament data through APIs
- Handling user inputs or external data sources
- Implementing authentication or authorization mechanisms
- Processing sensitive or classified information
- Designing rate limiting or abuse prevention
- Implementing audit logging and monitoring
- Writing security-critical validation logic
- Handling errors and exceptions securely
- Configuring network security or TLS
Security is foundational, not optional. Every feature must be designed with security in mind from the start, following the principle of "secure by default" and implementing defense-in-depth strategies aligned with Hack23 AB's ISMS policies.
Rules
- Never Trust Input: Validate and sanitize all user inputs - treat everything as potentially malicious
- Fail Securely: Systems must fail to a secure state, never expose sensitive information in errors
- Principle of Least Privilege: Grant minimal permissions required for functionality, nothing more
- Defense in Depth: Implement multiple overlapping security controls (validation + rate limiting + audit logging)
- Encrypt Everything: Use TLS 1.3+ for transit, AES-256 for at rest, never store secrets in plaintext
- Audit All Actions: Log authentication attempts, authorization failures, input validation errors
- Rate Limit Aggressively: Protect against abuse, DoS attacks, and resource exhaustion
- Validate Schema: Use strict schema validation for all MCP tool inputs and outputs
- Sanitize Output: Encode data appropriately to prevent injection attacks (XSS, command injection)
- Secure Defaults: All features must be secure by default, require opt-in for permissive settings
- No Secrets in Code: Never commit API keys, tokens, or credentials to source control
- Regular Updates: Keep dependencies updated, scan for vulnerabilities continuously
- Assume Breach: Design systems assuming attackers will get in - limit blast radius
- Document Security: Reference ISMS policies, document threat models and security controls
- Test Security: Write tests for authentication, authorization, input validation, and error handling
Examples
✅ Good Pattern: Comprehensive Input Validation
export class InputValidationService {
private readonly MAX_KEYWORD_LENGTH = 200;
private readonly MAX_DOCUMENT_ID_LENGTH = 50;
private readonly MAX_ARRAY_SIZE = 100;
private readonly SAFE_KEYWORD_PATTERN = /^[a-zA-Z0-9\s\-_]+$/;
private readonly DOCUMENT_ID_PATTERN = /^EP-\d{8}-\d{5}$/;
private readonly DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
private readonly ALLOWED_DOCUMENT_TYPES = new Set([
'REPORT',
'RESOLUTION',
'DECISION',
'DIRECTIVE',
'REGULATION',
'OPINION',
]);
validateSearchQuery(params: unknown): ValidatedSearchQuery {
if (typeof params !== 'object' || params === null) {
throw new ValidationError('Invalid parameters object');
}
const query = params as Record<string, unknown>;
return {
keywords: this.validateKeywords(query.keywords),
documentType: this.validateDocumentType(query.documentType),
dateFrom: this.validateDate(query.dateFrom, 'dateFrom'),
dateTo: this.validateDate(query.dateTo, 'dateTo'),
limit: this.validateLimit(query.limit),
};
}
private validateKeywords(value: unknown): string {
if (typeof value !== 'string') {
throw new ValidationError('Keywords must be a string');
}
const trimmed = value.trim();
if (trimmed.length === 0) {
throw new ValidationError('Keywords cannot be empty');
}
if (trimmed.length > this.MAX_KEYWORD_LENGTH) {
throw new ValidationError(
`Keywords exceed maximum length of ${this.MAX_KEYWORD_LENGTH} characters`
);
}
if (!this.SAFE_KEYWORD_PATTERN.test(trimmed)) {
throw new ValidationError(
'Keywords contain invalid characters. Only alphanumeric, spaces, hyphens, and underscores are allowed.'
);
}
return trimmed.replace(/\s+/g, ' ');
}
private validateDocumentType(value: unknown): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value !== 'string') {
throw new ValidationError('Document type must be a string');
}
const normalized = value.trim().toUpperCase();
if (!this.ALLOWED_DOCUMENT_TYPES.has(normalized)) {
throw new ValidationError(
`Invalid document type. Allowed types: ${Array.from(this.ALLOWED_DOCUMENT_TYPES).join(', ')}`
);
}
return normalized;
}
private validateDate(value: unknown, field: string): string | undefined {
if (value === undefined || value === null) {
return undefined;
}
if (typeof value !== 'string') {
throw new ValidationError(`${field} must be a string`);
}
if (!this.DATE_PATTERN.test(value)) {
throw new ValidationError(
`${field} must be in ISO 8601 format (YYYY-MM-DD)`
);
}
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new ValidationError(`${field} is not a valid date`);
}
const minDate = new Date('1952-01-01');
const maxDate = new Date();
maxDate.setDate(maxDate.getDate() + 1);
if (date < minDate || date > maxDate) {
throw new ValidationError(
`${field} must be between 1952-01-01 and today`
);
}
return value;
}
private validateLimit(value: unknown): number {
if (value === undefined || value === null) {
return 20;
}
const num = Number(value);
if (!Number.isFinite(num)) {
throw new ValidationError('Limit must be a valid number');
}
if (!Number.isInteger(num)) {
throw new ValidationError('Limit must be an integer');
}
if (num < 1 || num > 100) {
throw new ValidationError('Limit must be between 1 and 100');
}
return num;
}
validateDocumentId(value: unknown): string {
if (typeof value !== 'string') {
throw new ValidationError('Document ID must be a string');
}
const trimmed = value.trim();
if (trimmed.length > this.MAX_DOCUMENT_ID_LENGTH) {
throw new ValidationError('Document ID exceeds maximum length');
}
if (!this.DOCUMENT_ID_PATTERN.test(trimmed)) {
throw new ValidationError(
'Invalid document ID format. Expected: EP-YYYYMMDD-NNNNN'
);
}
return trimmed;
}
validateArray<T>(
value: unknown,
validator: (item: unknown) => T,
maxSize: number = this.MAX_ARRAY_SIZE
): T[] {
if (!Array.isArray(value)) {
throw new ValidationError('Value must be an array');
}
if (value.length === 0) {
throw new ValidationError('Array cannot be empty');
}
if (value.length > maxSize) {
throw new ValidationError(
`Array size exceeds maximum of ${maxSize} items`
);
}
return value.map((item, index) => {
try {
return validator(item);
} catch (error) {
throw new ValidationError(
`Invalid item at index ${index}: ${(error as Error).message}`
);
}
});
}
}
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
✅ Good Pattern: Rate Limiting with Multiple Strategies
export class RateLimiter {
private readonly fixedWindow = new Map<string, FixedWindowState>();
private readonly slidingWindow = new Map<string, number[]>();
private readonly MAX_REQUESTS = 100;
private readonly WINDOW_MS = 15 * 60 * 1000;
private readonly BURST_MAX = 10;
private readonly BURST_WINDOW_MS = 10 * 1000;
async checkLimit(clientId: string): Promise<void> {
const now = Date.now();
await this.checkBurstLimit(clientId, now);
await this.checkSustainedLimit(clientId, now);
this.recordRequest(clientId, now);
}
private async checkBurstLimit(clientId: string, now: number): Promise<void> {
const requests = this.slidingWindow.get(clientId) || [];
const recentBurst = requests.filter(
timestamp => now - timestamp < this.BURST_WINDOW_MS
);
if (recentBurst.length >= this.BURST_MAX) {
const oldestRequest = Math.min(...recentBurst);
const retryAfter = Math.ceil(
(oldestRequest + this.BURST_WINDOW_MS - now) / 1000
);
await this.logRateLimitViolation(clientId, 'burst', recentBurst.length);
throw new RateLimitError(
'Burst rate limit exceeded. Slow down your requests.',
retryAfter
);
}
}
private async checkSustainedLimit(clientId: string, now: number): Promise<void> {
const requests = this.slidingWindow.get(clientId) || [];
const recentRequests = requests.filter(
timestamp => now - timestamp < this.WINDOW_MS
);
if (recentRequests.length >= this.MAX_REQUESTS) {
const oldestRequest = Math.min(...recentRequests);
const retryAfter = Math.ceil(
(oldestRequest + this.WINDOW_MS - now) / 1000
);
await this.logRateLimitViolation(clientId, 'sustained', recentRequests.length);
throw new RateLimitError(
`Rate limit exceeded. Maximum ${this.MAX_REQUESTS} requests per 15 minutes. Try again in ${retryAfter} seconds.`,
retryAfter
);
}
}
private recordRequest(clientId: string, now: number): void {
const requests = this.slidingWindow.get(clientId) || [];
requests.push(now);
const filtered = requests.filter(
timestamp => now - timestamp < this.WINDOW_MS
);
this.slidingWindow.set(clientId, filtered);
}
private async logRateLimitViolation(
clientId: string,
type: 'burst' | 'sustained',
requestCount: number
): Promise<void> {
const event = {
timestamp: new Date().toISOString(),
eventType: 'RATE_LIMIT_VIOLATION',
severity: 'WARNING',
clientId: this.hashClientId(clientId),
limitType: type,
requestCount,
limit: type === 'burst' ? this.BURST_MAX : this.MAX_REQUESTS,
window: type === 'burst' ? this.BURST_WINDOW_MS : this.WINDOW_MS,
};
await auditLogger.log(event);
if (requestCount > this.MAX_REQUESTS * 2) {
await alerting.sendAlert({
type: 'SEVERE_RATE_LIMIT_VIOLATION',
clientId: this.hashClientId(clientId),
details: event,
});
}
}
private hashClientId(clientId: string): string {
return crypto
.createHash('sha256')
.update(clientId)
.digest('hex')
.substring(0, 16);
}
startCleanup(): void {
setInterval(() => {
const now = Date.now();
for (const [clientId, requests] of this.slidingWindow.entries()) {
const active = requests.filter(
timestamp => now - timestamp < this.WINDOW_MS
);
if (active.length === 0) {
this.slidingWindow.delete(clientId);
} else {
this.slidingWindow.set(clientId, active);
}
}
}, 60 * 1000);
}
}
export class RateLimitError extends Error {
constructor(
message: string,
public readonly retryAfter: number
) {
super(message);
this.name = 'RateLimitError';
}
}
✅ Good Pattern: Comprehensive Audit Logging
export class AuditLogger {
private readonly logStream: fs.WriteStream;
constructor(logPath: string) {
this.logStream = fs.createWriteStream(logPath, {
flags: 'a',
encoding: 'utf8',
});
process.on('SIGTERM', () => this.close());
process.on('SIGINT', () => this.close());
}
async log(event: AuditEvent): Promise<void> {
const logEntry = {
timestamp: new Date().toISOString(),
eventId: crypto.randomUUID(),
eventType: event.type,
severity: event.severity,
actor: this.sanitizeActor(event.actor),
action: event.action,
resource: event.resource,
outcome: event.outcome,
metadata: event.metadata,
ipAddress: event.ipAddress ? this.hashIp(event.ipAddress) : undefined,
};
return new Promise((resolve, reject) => {
this.logStream.write(
JSON.stringify(logEntry) + '\n',
(error) => {
if (error) {
console.error('CRITICAL: Audit log write failed:', error);
reject(error);
} else {
resolve();
}
}
);
});
}
async logAuthentication(
actor: string,
success: boolean,
metadata?: Record<string, unknown>
): Promise<void> {
await this.log({
type: 'AUTHENTICATION',
severity: success ? 'INFO' : 'WARNING',
actor,
action: 'authenticate',
resource: 'mcp-server',
outcome: success ? 'SUCCESS' : 'FAILURE',
metadata,
});
}
async logAuthorization(
actor: string,
action: string,
resource: string,
granted: boolean
): Promise<void> {
await this.log({
type: 'AUTHORIZATION',
severity: granted ? 'INFO' : 'WARNING',
actor,
action,
resource,
outcome: granted ? 'GRANTED' : 'DENIED',
});
}
async logValidationFailure(
actor: string,
tool: string,
errorType: string
): Promise<void> {
await this.log({
type: 'INPUT_VALIDATION',
severity: 'WARNING',
actor,
action: 'validate_input',
resource: tool,
outcome: 'FAILURE',
metadata: { errorType },
});
}
async logToolInvocation(
actor: string,
tool: string,
success: boolean,
duration?: number
): Promise<void> {
await this.log({
type: 'TOOL_INVOCATION',
severity: 'INFO',
actor,
action: 'invoke_tool',
resource: tool,
outcome: success ? 'SUCCESS' : 'FAILURE',
metadata: { duration },
});
}
private sanitizeActor(actor?: string): string {
if (!actor) {
return 'anonymous';
}
return crypto
.createHash('sha256')
.update(actor)
.digest('hex')
.substring(0, 16);
}
private hashIp(ip: string): string {
return crypto
.createHash('sha256')
.update(ip)
.digest('hex')
.substring(0, 16);
}
close(): void {
this.logStream.end();
}
}
interface AuditEvent {
type: string;
severity: 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
actor?: string;
action: string;
resource: string;
outcome: 'SUCCESS' | 'FAILURE' | 'GRANTED' | 'DENIED';
metadata?: Record<string, unknown>;
ipAddress?: string;
}
✅ Good Pattern: Secure Error Handling
export class SecureErrorHandler {
constructor(
private readonly auditLogger: AuditLogger
) {}
async handleToolError(
error: Error,
context: ErrorContext
): Promise<ToolResponse> {
await this.auditLogger.log({
type: 'TOOL_ERROR',
severity: 'ERROR',
actor: context.actor,
action: context.action,
resource: context.resource,
outcome: 'FAILURE',
metadata: {
errorType: error.name,
errorMessage: error.message,
stackTrace: error.stack,
},
});
const safeMessage = this.getSafeErrorMessage(error);
return {
isError: true,
content: [{
type: 'text',
text: safeMessage,
}],
};
}
private getSafeErrorMessage(error: Error): string {
if (error instanceof ValidationError) {
return error.message;
}
if (error instanceof RateLimitError) {
return error.message;
}
if (error instanceof AuthorizationError) {
return 'Access denied. Insufficient permissions.';
}
return 'An error occurred while processing your request. Please try again later.';
}
}
interface ErrorContext {
actor: string;
action: string;
resource: string;
}
export class AuthorizationError extends Error {
constructor(message: string = 'Access denied') {
super(message);
this.name = 'AuthorizationError';
}
}
❌ Bad Pattern: No Input Validation
export async function searchDocuments(query: any): Promise<Results> {
const sql = `SELECT * FROM documents WHERE title LIKE '%${query.keywords}%'`;
return await db.query(sql);
}
export async function getDocument(params: any): Promise<Document> {
const id = params.documentId as string;
return await europeanParliamentApi.getDocument(id);
}
export async function processText(text: string): Promise<string> {
return text.toUpperCase();
}
❌ Bad Pattern: Exposing Sensitive Information
export async function handleRequest(req: Request): Promise<Response> {
try {
return await processRequest(req);
} catch (error) {
return {
error: error.message,
stack: error.stack,
file: error.fileName,
};
}
}
export function validateInput(input: string): void {
if (input.includes('<script>')) {
throw new Error(`Invalid input: ${input}`);
}
}
export async function authenticate(username: string, password: string): Promise<boolean> {
console.log(`Authenticating user: ${username} with password: ${password}`);
return checkCredentials(username, password);
}
❌ Bad Pattern: No Rate Limiting
export async function searchDocuments(query: SearchQuery): Promise<Results> {
return await europeanParliamentApi.search(query);
}
export async function handleRequests(requests: Request[]): Promise<Response[]> {
return Promise.all(requests.map(r => processRequest(r)));
}
❌ Bad Pattern: Secrets in Code
const API_KEY = 'sk-1234567890abcdef';
const config = {
database: {
host: 'db.example.com',
username: 'admin',
password: 'SuperSecret123',
},
};
const url = `https://api.example.com/data?apikey=${API_KEY}`;
References
Security Standards
ISMS Policies
Core:
Supporting:
Security Tools
Cryptography
Remember
- Never trust input: Validate everything, sanitize always, encode appropriately
- Fail securely: Default deny, safe error messages, no information leakage
- Defense in depth: Multiple overlapping security controls
- Least privilege: Minimal permissions, need-to-know basis
- Audit everything: Log security events, monitor for anomalies
- Rate limit aggressively: Protect against abuse and DoS attacks
- Encrypt always: TLS 1.3+ for transit, AES-256 for at rest
- Secure by default: All features secure unless explicitly configured otherwise
- No secrets in code: Use environment variables, secret management systems
- Update dependencies: Patch vulnerabilities quickly, scan continuously
- Assume breach: Limit blast radius, segment networks, monitor for indicators
- Document security: Reference ISMS policies, explain threat mitigations
- Test security controls: Write tests for auth, authz, validation, encryption
- European Parliament data: Respect data protection regulations, handle sensitively
- MCP protocol: Validate tool schemas strictly, sanitize responses