Skip to content

Latest commit

 

History

History
904 lines (700 loc) · 27.7 KB

File metadata and controls

904 lines (700 loc) · 27.7 KB

Security Documentation

This document explains the security measures implemented in the Codingbuddy MCP Server to protect against common attack vectors.

Table of Contents


DoS Protection

File Size Limits (SEC-004)

Threat Model: Attackers could attempt to exhaust server resources by providing extremely large configuration files, leading to:

  • Memory exhaustion (Out of Memory errors)
  • CPU exhaustion (parsing large JSON/INI files)
  • Disk I/O flooding (blocking the event loop)

Protection Mechanism: 1MB file size limit for all configuration files.

Why 1MB?

The 1MB limit is calibrated based on real-world configuration file sizes:

Config Type Typical Size Large Config Edge Case
Simple .editorconfig 500 bytes - 2KB 5KB < 10KB
Simple tsconfig.json 500 bytes - 5KB 10KB - 50KB < 100KB
Complex monorepo tsconfig.json 10KB - 50KB 50KB - 100KB < 200KB
Large ESLint config 20KB - 80KB 80KB - 150KB < 200KB
Prettier config 1KB - 10KB 10KB - 20KB < 50KB
MarkdownLint config 1KB - 5KB 5KB - 20KB < 50KB

Analysis:

  • 99.9% of legitimate configs are < 200KB
  • 1MB provides 5x headroom beyond the largest edge case
  • Practical for complex monorepo configurations
  • Blocks pathological attack payloads (100MB+ files)

Attack Vectors Prevented

  1. Memory Exhaustion Attack

    • Vector: Attacker creates 100MB+ config file to OOM the server
    • Mitigation: File size checked via fs.stat() before reading content
    • Result: Attack file rejected; default config returned
  2. CPU DoS Attack

    • Vector: Large JSON parsing consumes excessive CPU time
    • Mitigation: 1MB limit ensures parsing completes in milliseconds
    • Result: Server remains responsive under attack
  3. Disk I/O Flooding

    • Vector: Reading massive files blocks Node.js event loop
    • Mitigation: Size check prevents actual file read operation
    • Result: No I/O blocking; default config returned immediately

Performance Impact

The security checks (file size validation) introduce minimal overhead to file operations:

Measured Performance Overhead (from benchmark tests):

Operation Overhead Acceptable Threshold Status
Small files (<1KB) 10-20% <20% ✅ Within limits
Medium files (~100KB) 6-14% <10% ⚠️ Marginal
Path validation ~4μs avg <100μs ✅ Excellent
Early rejection (oversized) <0.02ms <5ms ✅ Excellent

Why Double I/O?

The implementation calls fs.stat() before fs.readFile(), resulting in two I/O operations:

  1. First I/O (fs.stat()): Check file size (fast, metadata only)
  2. Second I/O (fs.readFile()): Read file content (slower, full data)

Justification:

  • Fail-Fast Protection: Reject large files before reading them into memory
  • DoS Prevention: Prevents memory exhaustion from multi-GB attack files
  • Acceptable Overhead: 10-20% overhead is negligible compared to DoS risk
  • Performance Scales: Overhead decreases for larger files (amortized over read time)

Early Rejection Performance:

  • Oversized files rejected in <0.02ms (single stat() call, no read())
  • Memory saved: Prevents loading multi-GB files
  • Security tradeoff: Small performance cost for significant security benefit

For production deployments where performance is critical, consider:

  • Caching file metadata to avoid repeated stat() calls
  • Using streaming APIs for very large legitimate files
  • Monitoring overhead in performance budgets

Implementation

// File: src/shared/file.utils.ts
import { formatBytes } from './format.utils';
import { AccessibleErrorResponse } from './validation.constants';

/**
 * Custom error class for file size violations
 * ACC-003: Implements AccessibleErrorResponse for downstream UI consumption
 */
export class FileSizeError extends Error implements AccessibleErrorResponse {
  readonly code = 'FILE_SIZE_EXCEEDED';
  readonly userMessage: string;
  readonly technicalMessage: string;
  readonly accessibilityHints = {
    role: 'alert' as const,
    live: 'assertive' as const,
    announce: true,
  };
  readonly suggestions: string[];

  constructor(actualSize: number, maxSize: number, filePath?: string) {
    const userMsg = `File too large (${formatBytes(actualSize)}). Maximum size is ${formatBytes(maxSize)}.`;
    super(userMsg);

    this.userMessage = userMsg;
    this.technicalMessage = `File size ${actualSize} bytes exceeds maximum ${maxSize} bytes${filePath ? ` (${filePath})` : ''}`;
    this.suggestions = [
      'Use a smaller file',
      'Compress the file before uploading',
      `Split file into chunks under ${formatBytes(maxSize)}`,
    ];

    this.name = 'FileSizeError';
  }
}

async function validateFileSize(
  filePath: string,
  maxSize: number,
): Promise<void> {
  const stats = await fs.stat(filePath);
  if (stats.size > maxSize) {
    throw new FileSizeError(stats.size, maxSize, filePath);
  }
}

// File: src/shared/validation.constants.ts
export const MAX_CONFIG_FILE_SIZE = 1024 * 1024; // 1MB

// File: src/shared/format.utils.ts
export function formatBytes(bytes: number): string {
  if (bytes === 0) return '0 bytes';
  if (bytes === 1) return '1 byte';

  const units = ['bytes', 'KB', 'MB', 'GB', 'TB'];
  let size = bytes;
  let unitIndex = 0;

  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }

  const formatted = unitIndex === 0
    ? size.toString()
    : size.toFixed(2).replace(/\.?0+$/, '');

  return `${formatted} ${units[unitIndex]}`;
}

Behavior on Violation

When a config file exceeds 1MB:

  1. No error thrown - Graceful degradation
  2. Default config returned - Fail-safe behavior
  3. Warning logged - Includes configType, filePath, size, maxSize
  4. Service continues - No downtime or error propagation

Example Log:

{
  "level": "warn",
  "message": "Config file exceeds size limit",
  "configType": "typescript",
  "filePath": "/path/to/tsconfig.json",
  "size": 2097152,
  "maxSize": 1048576
}

Input Validation

Query and Prompt Validation

Threat Model: Injection attacks, resource exhaustion, command injection.

Protection Mechanisms:

1. Length Limits

export const MAX_QUERY_LENGTH = 1000;      // Search queries
export const MAX_PROMPT_LENGTH = 10000;    // PLAN/ACT/EVAL prompts
export const MAX_AGENT_NAME_LENGTH = 100;  // Agent identifiers

Rationale:

  • 1000 chars for queries: Sufficient for complex search terms; blocks abuse
  • 10000 chars for prompts: Allows detailed task descriptions; prevents memory exhaustion
  • 100 chars for agent names: Practical identifier length; prevents path traversal

2. Pattern Validation

Agent Name Pattern: /^[a-z0-9-]+$/

Allowed: frontend-developer, code-reviewer, test-strategy-specialist Blocked:

  • Uppercase characters (prevents case-sensitivity issues)
  • Spaces (prevents filename issues)
  • Special characters (prevents injection attacks)
  • Path separators (prevents directory traversal: ../../malicious)

3. Type Guards

Comprehensive type validation prevents type confusion attacks:

// String validation
export function isNonEmptyString(value: unknown): value is string {
  return typeof value === 'string' && value.trim().length > 0;
}

// Array validation
export function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === 'string');
}

// Enum validation
export function isValidMode(value: unknown): value is ValidMode {
  return typeof value === 'string' && VALID_MODES.includes(value as ValidMode);
}

4. EditorConfig Numeric Validation

Decimal Rejection: Prevents invalid configuration values.

export function parseIndentSize(value: string): ParseResult<number> {
  // Reject decimal numbers
  if (value.includes('.')) {
    return {
      success: false,
      error: 'Indent size must be an integer, decimals are not allowed',
    };
  }

  const parsed = parseInt(value, 10);
  if (isNaN(parsed) || parsed < 1 || !Number.isInteger(parsed)) {
    return {
      success: false,
      error: 'Indent size must be a positive integer (>= 1)',
    };
  }

  return { success: true, value: parsed };
}

Protected Against:

  • Decimal values: 2.5, 4.0, 3.14
  • Scientific notation: 1e2, 2E3
  • Non-numeric values: abc, null, undefined
  • Negative/zero values: -1, 0

Enhanced Validation Messages

All validation errors include:

  1. Clear error description
  2. Actionable guidance on how to fix
  3. Example of correct format

Example:

{
  valid: false,
  error: 'Agent name must contain only lowercase letters, numbers, and hyphens. Example: "frontend-developer" or "code-reviewer"'
}

Path Traversal Prevention

Threat Model (SEC-005)

Attack: Injecting path traversal patterns (e.g., ../, ..\\, null bytes) to access files outside allowed directories.

Impact:

  • Unauthorized file access (e.g., /etc/passwd, application secrets)
  • Information disclosure
  • Potential remote code execution if sensitive files are compromised
  • Directory traversal attacks

Protection Mechanisms

Pre-validation using validateFilePath() function:

// File: src/shared/file.utils.ts
export function validateFilePath(
  filePath: string,
  options?: { allowedBasePath?: string },
): void {
  // Check for null bytes (common in path traversal attacks)
  if (filePath.includes('\0')) {
    throw new PathTraversalError(filePath, 'Null byte detected');
  }

  // Check for path traversal patterns as path components
  // Split by both Unix (/) and Windows (\) separators
  const segments = filePath.split(/[/\\]/);
  for (const segment of segments) {
    if (segment === '..') {
      throw new PathTraversalError(
        filePath,
        'Path traversal pattern detected (..)',
      );
    }
  }

  // If base path is specified, ensure file is within allowed directory
  if (options?.allowedBasePath) {
    const resolvedPath = resolve(filePath);
    const resolvedBase = resolve(options.allowedBasePath);

    // Normalize to handle trailing slashes consistently
    const normalizedPath = normalize(resolvedPath);
    const normalizedBase = normalize(resolvedBase);

    // Check if resolved path starts with base path
    // This prevents escaping via symlinks or absolute paths
    if (!normalizedPath.startsWith(normalizedBase + sep) &&
        normalizedPath !== normalizedBase) {
      throw new PathTraversalError(
        filePath,
        `Path escapes allowed base directory "${options.allowedBasePath}"`,
      );
    }
  }
}

Protected Patterns:

  • ✅ Null bytes (\0)
  • ✅ Parent directory references (.. as path segment)
  • ✅ Absolute paths escaping base directory
  • ✅ Symlink-based escapes (via path resolution)

Implementation

Automatic Protection in File Reading Functions:

// File: src/shared/file.utils.ts

// Example: safeReadFile with path validation
export async function safeReadFile(
  filePath: string,
  options?: FileReadOptions,
): Promise<string | null> {
  try {
    // Validate file path if allowedBasePath is specified
    if (options?.allowedBasePath !== undefined) {
      validateFilePath(filePath, { allowedBasePath: options.allowedBasePath });
    }

    // Check file size if maxSize is specified
    if (options?.maxSize !== undefined) {
      await validateFileSize(filePath, options.maxSize);
    }

    return await fs.readFile(filePath, 'utf-8');
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
      return null;
    }
    throw error;
  }
}

Usage Example:

// Secure file reading with path validation
const content = await safeReadFile('/project/config/app.json', {
  allowedBasePath: '/project/config',
  maxSize: 1024 * 1024,
});

// This will throw PathTraversalError:
await safeReadFile('/project/../etc/passwd', {
  allowedBasePath: '/project/config',
});

Attack Vectors Prevented

  1. Parent Directory Traversal

    • Vector: ../../etc/passwd or ..\..\..\Windows\System32\config\SAM
    • Mitigation: .. segments detected and rejected before path resolution
    • Result: PathTraversalError thrown
  2. Null Byte Injection

    • Vector: config.json\0.txt (tries to bypass extension checks)
    • Mitigation: Null bytes (\0) detected and rejected immediately
    • Result: PathTraversalError thrown
  3. Absolute Path Escape

    • Vector: /etc/passwd when allowed base is /project/config
    • Mitigation: Path resolution + base path validation
    • Result: PathTraversalError thrown
  4. Symlink Escape

    • Vector: Symlink pointing outside allowed directory
    • Mitigation: resolve() canonicalizes paths, base path check catches escape
    • Result: PathTraversalError thrown

Behavior on Detection

  1. Dangerous path detected → Throw PathTraversalError
  2. Error logged (if logger provided in tryReadFile)
  3. No file operation attempted → Attack blocked before I/O
  4. Clear error message → Includes path and specific violation

Example Error:

PathTraversalError: Path traversal detected in "../../../etc/passwd": Path traversal pattern detected (..)

Example Log (tryReadFile with logger):

{
  "level": "warn",
  "message": "tryReadFile: Path traversal attempt (silent failure)",
  "code": "PATH_TRAVERSAL_DETECTED",
  "filePath": "/project/../etc/passwd",
  "message": "Path escapes allowed base directory \"/project/config\""
}

Integration Guidance for Developers

When to Use Path Validation:

  • Reading user-supplied file paths
  • File upload handlers
  • Configuration file loaders
  • Any file operation with external input

How to Integrate:

// Option 1: Automatic validation (recommended)
const content = await safeReadFile(userProvidedPath, {
  allowedBasePath: '/safe/directory',
  maxSize: 1024 * 1024,
});

// Option 2: Manual validation
validateFilePath(userProvidedPath, { allowedBasePath: '/safe/directory' });
const content = await fs.readFile(userProvidedPath, 'utf-8');

Best Practices:

  • Always specify allowedBasePath for user-controlled paths
  • Use most restrictive base path possible
  • Combine with file size limits (maxSize) for defense in depth
  • Log path traversal attempts for security monitoring

Test Coverage Requirements

When adding path traversal protection:

  • Test valid paths within allowed base directory
  • Test .. segment rejection (Unix and Windows)
  • Test null byte rejection (\0)
  • Test absolute path escape detection
  • Test accessing base directory itself (should be allowed)
  • Test symlink escape prevention
  • Test error messages are descriptive
  • Test logger integration for silent failures

Prototype Pollution Prevention

Threat Model

Attack: Injecting __proto__, constructor, or prototype keys into JSON config files to pollute JavaScript prototypes.

Impact:

  • Remote Code Execution (RCE)
  • Privilege escalation
  • DoS via prototype manipulation

Protection Mechanism

Pre-parse validation using containsDangerousKeys() function:

// File: src/shared/security.utils.ts
export function containsDangerousKeys(
  obj: any,
  path: string = '',
): string | null {
  for (const key in obj) {
    const currentPath = path ? `${path}.${key}` : key;

    // Check for dangerous keys
    if (DANGEROUS_KEYS.has(key)) {
      return currentPath;
    }

    // Recursively check nested objects
    if (typeof obj[key] === 'object' && obj[key] !== null) {
      const dangerousPath = containsDangerousKeys(obj[key], currentPath);
      if (dangerousPath) return dangerousPath;
    }
  }

  return null;
}

Dangerous Keys Blocked:

  • __proto__
  • constructor
  • prototype

Implementation

Applied to all JSON config parsers:

// Example: TypeScript config parsing
const parsed = JSON.parse(content);

// Check for prototype pollution before any processing
const dangerousPath = containsDangerousKeys(parsed);
if (dangerousPath) {
  this.logger.warn('Dangerous key detected in TypeScript config', {
    filePath,
    dangerousPath,
  });
  return DEFAULT_TYPESCRIPT_CONFIG;
}

Protected Config Types:

  • ✅ TypeScript (tsconfig.json)
  • ✅ ESLint (.eslintrc.json, .eslintrc)
  • ✅ Prettier (.prettierrc, .prettierrc.json)
  • ✅ MarkdownLint (.markdownlint.json)

Behavior on Detection

  1. Dangerous key detected → Return default config
  2. Warning logged → Includes filePath and dangerousPath
  3. No parsing attempted → Malicious payload never executed
  4. Service continues → Graceful degradation

Example Attack Attempt:

{
  "__proto__": { "polluted": true },
  "compilerOptions": { "strict": true }
}

Example Log:

{
  "level": "warn",
  "message": "Dangerous key detected in TypeScript config",
  "filePath": "/path/to/tsconfig.json",
  "dangerousPath": "__proto__"
}

Error Handling

Structured Logging

All errors are logged with structured context for security monitoring:

this.logger.warn('Failed to parse config file', {
  configType,    // Type of config (typescript, eslint, etc.)
  filePath,      // Path to config file
  errorCode,     // Node.js error code (ENOENT, EACCES, etc.)
  errorMessage,  // Human-readable error description
});

Security Benefits:

  • Audit trail for failed parse attempts
  • Attack pattern detection via log aggregation
  • Incident response with full context

Error Sanitization

ENOENT (File Not Found):

  • Logged at debug level (not warn)
  • Prevents log flooding from legitimate missing files

Other Errors:

  • Logged at warn level with full context
  • Enables detection of permission errors, corruption, malformed JSON

Silent Failures vs Explicit Errors

Two error handling strategies:

1. safeReadFile() - Explicit Error Handling

  • Returns null for ENOENT (file not found)
  • Throws for all other errors (permission denied, size violations, etc.)
  • Use when errors should be handled by caller

2. tryReadFile() - Silent Failure

  • Returns undefined for all errors
  • Logs size violations for observability
  • Use for optional/fallback operations

Schema Validation

Zod Integration

All JSON config files are validated using Zod schemas after prototype pollution checks:

import { TypeScriptConfigSchema } from './conventions.schemas';

// 1. Parse JSON
const parsed = JSON.parse(content);

// 2. Check for prototype pollution
const dangerousPath = containsDangerousKeys(parsed);
if (dangerousPath) return DEFAULT_CONFIG;

// 3. Validate with Zod schema
const validationResult = TypeScriptConfigSchema.safeParse(parsed);
if (!validationResult.success) {
  this.logger.warn('TypeScript config validation failed', {
    filePath,
    errors: validationResult.error.errors,
  });
  return DEFAULT_CONFIG;
}

// 4. Use validated config
const config = validationResult.data;

Benefits:

  • Type-safe config objects
  • Injection prevention via schema constraints
  • Graceful degradation on validation failure
  • Detailed error messages for debugging

Performance Considerations

LRU Cache with mtime Validation

Threat: Cache poisoning via file replacement attacks.

Protection: Modification time (mtime) validation prevents stale cache hits.

// Cache key: project root path
// Cache validation: file modification times
const mtimes = await this.getConfigMtimes(projectRoot);
const cached = this.conventionsCache.get(projectRoot, mtimes);

Invalidation Strategy:

  1. TTL-based: 5-minute expiration
  2. mtime-based: Invalidate if any config file changes

Attack Mitigation:

  • Attacker replaces tsconfig.json → Cache invalidated (mtime changed)
  • Attacker waits > 5 minutes → Cache expired (TTL)
  • Result: Fresh config parsed with all security checks

Accessibility

AccessibleErrorResponse Pattern (ACC-003)

Purpose: The AccessibleErrorResponse interface ensures error messages are accessible to users with disabilities, particularly those using assistive technologies like screen readers.

WCAG 2.1 AA Compliance: This pattern addresses the following Web Content Accessibility Guidelines (WCAG 2.1 Level AA) success criteria:

  • SC 3.3.1 Error Identification: Errors are clearly identified and described to the user in text
  • SC 3.3.3 Error Suggestion: When errors are detected, suggestions for correction are provided
  • SC 4.1.3 Status Messages: Status messages (including errors) can be programmatically determined and announced to assistive technologies

Interface Definition

interface AccessibleErrorResponse {
  // Machine-readable error code for programmatic handling
  code: string;

  // User-friendly error message (no technical jargon or sensitive data)
  userMessage: string;

  // Detailed technical message for logging and debugging
  technicalMessage: string;

  // ARIA attributes for screen reader integration
  accessibilityHints: {
    // ARIA role for the error element (typically 'alert')
    role: 'alert' | 'status';

    // ARIA live region politeness level
    // - 'assertive': interrupts current speech (for critical errors)
    // - 'polite': waits for current speech to finish
    live: 'assertive' | 'polite';

    // Whether screen readers should announce this error immediately
    announce: boolean;
  };

  // Actionable suggestions for users to resolve the error
  suggestions: string[];
}

Implementation Examples

FileSizeError (SEC-004 integration):

class FileSizeError extends Error implements AccessibleErrorResponse {
  readonly code = 'FILE_SIZE_EXCEEDED';
  readonly userMessage = 'File too large (2 MB). Maximum size is 1 MB.';
  readonly technicalMessage = 'File size 2097152 bytes exceeds maximum 1048576 bytes';
  readonly accessibilityHints = {
    role: 'alert' as const,
    live: 'assertive' as const,
    announce: true,
  };
  readonly suggestions = [
    'Use a smaller file',
    'Compress the file before uploading',
    'Split file into chunks under 1 MB',
  ];
}

PathTraversalError (SEC-005 integration):

class PathTraversalError extends Error implements AccessibleErrorResponse {
  readonly code = 'PATH_TRAVERSAL_DETECTED';
  readonly userMessage = 'Invalid file path: path traversal attempt detected';
  readonly technicalMessage = 'Path traversal detected in "/project/../etc/passwd": Path traversal pattern detected (..)';
  readonly accessibilityHints = {
    role: 'alert' as const,
    live: 'assertive' as const,
    announce: true,
  };
  readonly suggestions = [
    'Use file paths without .. components',
    'Use absolute paths within allowed directories',
    'Avoid null bytes and special characters in file paths',
  ];
}

Frontend Integration

When consuming AccessibleErrorResponse errors in UI components, use the ARIA attributes to create accessible error displays:

// React example
function ErrorDisplay({ error }: { error: AccessibleErrorResponse }) {
  return (
    <div
      role={error.accessibilityHints.role}
      aria-live={error.accessibilityHints.live}
      aria-atomic="true"
    >
      <p className="error-message">{error.userMessage}</p>
      {error.suggestions.length > 0 && (
        <ul className="error-suggestions" aria-label="Suggestions to fix this error">
          {error.suggestions.map((suggestion, i) => (
            <li key={i}>{suggestion}</li>
          ))}
        </ul>
      )}
    </div>
  );
}

Benefits

  1. Screen Reader Compatibility: Errors are announced immediately with appropriate politeness level
  2. Clear Error Communication: User messages are free of technical jargon and sensitive information
  3. Actionable Guidance: Suggestions provide users with clear steps to resolve errors
  4. Security Information Separation: Technical details are logged separately from user-facing messages
  5. Programmatic Error Handling: Machine-readable error codes enable automated error handling

WCAG 2.1 AA Compliance Checklist

  • Error Identification (SC 3.3.1): User messages clearly identify and describe errors
  • Error Suggestion (SC 3.3.3): Suggestions array provides correction guidance
  • Status Messages (SC 4.1.3): ARIA attributes enable programmatic determination and announcement
  • Name, Role, Value (SC 4.1.2): Role and live attributes properly set
  • Parsing (SC 4.1.1): Structured interface ensures valid markup integration

References


Reporting Security Issues

If you discover a security vulnerability, please email:

Email: security@codingbuddy.io PGP Key: [Link to PGP key]

Please include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if available)

Response Time: We aim to respond within 48 hours.

Disclosure Policy: We follow responsible disclosure. Please do not publicly disclose vulnerabilities before we've had a chance to address them.


Security Checklist for Developers

When adding new config file support:

  • Implement 1MB file size limit via parseConfigWithDefaults()
  • Add prototype pollution check with containsDangerousKeys()
  • Add path traversal protection with validateFilePath() or allowedBasePath option
  • Create Zod validation schema
  • Use structured logging for errors
  • Return default config on validation failure (graceful degradation)
  • Add comprehensive test coverage:
    • Happy path (valid config)
    • File size violation (> 1MB)
    • Prototype pollution attempt (__proto__, constructor, prototype)
    • Path traversal attempt (.. segments, null bytes, base path escape)
    • Schema validation failure
    • File not found (ENOENT)
    • Permission denied (EACCES)

When adding user-controlled file operations:

  • Specify allowedBasePath for all user-provided file paths
  • Use most restrictive base path possible
  • Combine path validation with file size limits for defense in depth
  • Test all path traversal attack vectors (see SEC-005 test requirements)
  • Log path traversal attempts for security monitoring

References


Last Updated: 2026-01-08 Version: 1.0.0