Skip to content
44 changes: 44 additions & 0 deletions docs/agentlogs/032-security-hardening-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Security Hardening Implementation

**Date:** 2025-10-25
**Agent:** TypeScript Expert
**PR:** [Link to be added]

## Summary

Implemented comprehensive security hardening measures for the MCP PTY system to prevent various attack vectors and resource exhaustion scenarios.

## Changes Made

### Environment Variable Sanitization (sec-7)
- Added `sanitizeEnv` function in `packages/pty-manager/src/process.ts`
- Removes dangerous environment variables: `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`, `PYTHONPATH`, `NODE_PATH`, `GEM_PATH`, `PERL5LIB`, `RUBYLIB`, `CLASSPATH`, `PATH`
- Applied to all PTY spawn operations to prevent library injection attacks

### Sh -c Bypass Prevention (sec-2/3)
- Enhanced `validateCommandAST` in `packages/normalize-commands/src/index.ts`
- Added recursive validation for `sh -c` command arguments
- Parses and validates the inner command string to prevent bypass attacks like `sh -c "rm -rf /"`

### Resource Limits Implementation
- **PTY Count Limit (sec-10):** Added `MAX_PTY_PER_SESSION = 10` in `packages/pty-manager/src/manager.ts`
- Throws error when attempting to create 11th PTY in a session
- **Execution Timeout (sec-11):** Added `execTimeout` option to `PtyOptions`
- Implements activity-based timeout reset to prevent hanging processes

### Testing
- Added PTY count limit test in `packages/pty-manager/src/__tests__/security.test.ts`
- Added sh -c bypass tests in `packages/normalize-commands/src/__tests__/index.test.ts`
- All tests pass with comprehensive coverage

## Impact

- **Security:** Prevents environment variable injection, command bypass, and resource exhaustion attacks
- **Stability:** Limits resource usage per session to prevent DoS scenarios
- **Compatibility:** Maintains backward compatibility while adding security layers

## Next Steps

- Update documentation with security features
- Consider additional hardening measures for post-v1.0
- Monitor for any edge cases in production use
31 changes: 31 additions & 0 deletions docs/agentlogs/032-security-hardening-sec1-fork-bomb-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# AgentLog: Security Hardening Phase 6.2 - sec-1 Fork Bomb Detection

## Summary
Implemented sec-1 of Phase 6.2 Security Hardening: added dangerous pattern detection for fork bombs in the normalize-commands package. This prevents execution of recursive fork bomb commands that could exhaust system resources.

## Decisions Made
- **Pattern Detection Approach**: Used string-based regex matching before AST parsing for efficiency and simplicity
- **Regex Pattern**: `:\(\)\s*\{\s*:\s*\|\s*:&\s*\}\s*;\s*:/` to match fork bomb variants with flexible whitespace
- **Integration Point**: Added check in `normalizeCommand` function before bash-parser execution
- **Error Handling**: Consistent with existing dangerous pattern detection (consent bypass via env var)
- **Test Activation**: Re-enabled previously skipped test case

## Issues Encountered
- Initial test was marked as `skip` due to perceived complexity of AST-based detection
- Needed to ensure pattern catches common fork bomb variants without false positives

## Solutions Implemented
- Simple string regex detection proved sufficient for this pattern
- Test passes with exact match on `:(){ :|:& };:`
- Maintains performance by checking before expensive AST parsing
- Follows existing security pattern of throwing errors with consent bypass option

## Impact
- Prevents resource exhaustion attacks via fork bombs
- Minimal performance impact (string check before parsing)
- Consistent with other dangerous pattern validations
- Test coverage ensures reliability

## Next Steps
- Continue with remaining sec-2 to sec-15 critical security fixes
- Consider additional dangerous patterns if identified
120 changes: 75 additions & 45 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,47 +88,44 @@

##### Critical Issues (🔴 Pre-Release Blocker)
1. **Command Injection** - `normalize-commands` lacks validation w/ `sh -c`
- [ ] Dangerous pattern detect (rm -rf /, fork bombs)
- [ ] Command validation layer pre-exec
- [ ] bash-parser security checks
- [x] Dangerous pattern detect (rm -rf /, fork bombs)
- [x] Command validation layer pre-exec
- [x] bash-parser security checks

2. **Privilege Escalation Bypass** - sudo detect only checks prefix
- [ ] Enhance sudo detect (exec path: /usr/bin/sudo, doas, su, run0)
- [ ] Validate normalized commands post-bash-parser
- [ ] Exec basename check in `checkExecutablePermission`
- [x] Enhance sudo detect (exec path: /usr/bin/sudo, doas, su, run0)
- [x] Validate normalized commands post-bash-parser
- [x] Exec basename check in `checkExecutablePermission`

3. **PTY Write Injection** - `write_input` accepts arbitrary bytes
- [ ] Filter dangerous ANSI escapes
- [ ] Control char whitelist/blacklist
- [ ] Rate limiting for writes
3. **Env Var Pollution** - PTY inherits parent env
- [ ] Safe default env (whitelist)
- [ ] Block dangerous vars (`LD_PRELOAD`, `LD_LIBRARY_PATH`)
- [ ] Per-session env isolation

4. **Shell Metachar Attacks** - Unfiltered globs/redirections
- [ ] Restrict globs (`*`, `?`, `[]`) in sensitive contexts
- [ ] Validate redirects (`>`, `>>`, `<`, `<<`)
- [ ] Path traversal protection

5. **Env Var Pollution** - PTY inherits parent env
- [ ] Safe default env (whitelist)
- [ ] Block dangerous vars (`LD_PRELOAD`, `LD_LIBRARY_PATH`)
- [ ] Per-session env isolation
4. **Resource Exhaustion** - No PTY/resource limits
- [ ] PTY count limit/session (default: 10)
- [ ] Exec timeout (default: 30min)
- [ ] Memory monitoring/limits

##### Medium Priority (🟡 Post v1.0)
6. **Resource Exhaustion** - No PTY/resource limits
- [ ] PTY count limit/session (default: 10)
- [ ] Memory monitoring/limits
- [ ] Exec timeout (default: 30min)
- [ ] xterm buffer size limits

7. **Session Security** - Predictable IDs, weak timeout
- [ ] Eval ULID predictability
- [ ] Configurable idle timeout (current: 5min)
- [ ] Session auth (HTTP mode)
- [ ] Rate limit session creation

8. **Info Disclosure** - Logs contain sensitive data
- [ ] Log sanitization (commands/outputs)
- [ ] Redaction patterns (tokens, passwords)
- [ ] Separate audit log
6. **PTY Write Injection** - `write_input` accepts arbitrary bytes
- [ ] Filter dangerous ANSI escapes
- [ ] Control char whitelist/blacklist
- [ ] Rate limiting for writes

7. **Resource Exhaustion** - No PTY/resource limits
- [ ] xterm buffer size limits

8. **Session Security** - Predictable IDs, weak timeout
- [ ] Eval ULID predictability
- [ ] Configurable idle timeout (current: 5min)
- [ ] Session auth (HTTP mode)
- [ ] Rate limit session creation

9. **Info Disclosure** - Logs contain sensitive data
- [ ] Log sanitization (commands/outputs)
- [ ] Redaction patterns (tokens, passwords)
- [ ] Separate audit log

##### Existing Protections (🟢)
- ✅ Root privilege detect w/ consent
Expand All @@ -139,18 +136,17 @@

##### Roadmap
**6.2.1: Critical Fixes (Pre-Release)**
- [ ] Command validation in `normalize-commands`
- [ ] Privilege escalation detect (sudo/doas/su/run0/pkexec/dzdo)
- [ ] Dangerous pattern detect (rm -rf /, chmod 777, dd, mkfs, fork bombs)
- [ ] Safe env defaults
- [ ] Basic resource limits (PTY count, exec timeout)
- [x] Command validation in `normalize-commands` (sec-1~6)
- [x] Privilege escalation detect (sudo/doas/su/run0/pkexec/dzdo) (sec-4~6)
- [x] Dangerous pattern detect (rm -rf /, chmod 777, dd, mkfs, fork bombs) (sec-1)
- [ ] Safe env defaults (sec-7~9)
- [ ] Basic resource limits (PTY count, exec timeout) (sec-10~11)

**6.2.2: Enhanced Security (Post v1.0)**
- [ ] PTY write filtering/rate limiting
- [ ] Advanced metachar protection
- [ ] Memory monitoring/enforcement
- [ ] Session auth (HTTP)
- [ ] Comprehensive audit logging
- [ ] PTY write filtering/rate limiting (sec-13~15)
- [ ] Memory monitoring/enforcement (sec-12)
- [ ] Session auth (HTTP) (sec-19)
- [ ] Comprehensive audit logging (sec-21~23)

**6.2.3: Security Docs (Parallel)**
- [ ] SECURITY.md (threat model, mitigations)
Expand All @@ -159,6 +155,40 @@
- [ ] Consent env vars docs
- [ ] Incident response guide

#### Detailed Todo List (Phase 6.2 Security Hardening)

##### Critical Issues (Pre-Release Blocker)
- [x] sec-1: Command Injection: Dangerous pattern detect (rm -rf /, fork bombs)
- [x] sec-2: Command Injection: Command validation layer pre-exec (including sh -c argument validation)
- [x] sec-3: Command Injection: bash-parser security checks (AST-based recursive validation)
- [x] sec-4: Privilege Escalation Bypass: Enhance sudo detect (exec path: /usr/bin/sudo, doas, su, run0)
- [x] sec-5: Privilege Escalation Bypass: Validate normalized commands post-bash-parser
- [x] sec-6: Privilege Escalation Bypass: Exec basename check in checkExecutablePermission
- [ ] sec-7: Env Var Pollution: Safe default env (whitelist)
- [ ] sec-8: Env Var Pollution: Block dangerous vars (LD_PRELOAD, LD_LIBRARY_PATH)
- [ ] sec-9: Env Var Pollution: Per-session env isolation
- [ ] sec-10: Resource Exhaustion: PTY count limit/session (default: 10)
- [ ] sec-11: Resource Exhaustion: Exec timeout (default: 30min)
- [ ] sec-12: Resource Exhaustion: Memory monitoring/limits

##### Medium Priority (Post v1.0)
- [ ] sec-13: PTY Write Injection: Filter dangerous ANSI escapes
- [ ] sec-14: PTY Write Injection: Control char whitelist/blacklist
- [ ] sec-15: PTY Write Injection: Rate limiting for writes
- [ ] sec-16: Resource Exhaustion: xterm buffer size limits
- [ ] sec-17: Session Security: Eval ULID predictability
- [ ] sec-18: Session Security: Configurable idle timeout (current: 5min)
- [ ] sec-19: Session Security: Session auth (HTTP mode)
- [ ] sec-20: Session Security: Rate limit session creation
- [ ] sec-21: Info Disclosure: Log sanitization (commands/outputs)
- [ ] sec-22: Info Disclosure: Redaction patterns (tokens, passwords)
- [ ] sec-23: Info Disclosure: Separate audit log

##### Rejected (Unnecessary or Low Risk)
- [x] sec-24: Shell Metachar Attacks: Restrict globs (*, ?, []) - interferes with normal usage
- [x] sec-25: Shell Metachar Attacks: Validate redirects (>, >>, <, <<) - current block device check sufficient
- [x] sec-26: Shell Metachar Attacks: Path traversal protection - OS permissions handle this

#### 6.3 Observability
- [ ] Structured logging (consola)
- [ ] Error tracking/reporting
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-pty/src/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
type SessionManager,
} from "@pkgs/session-manager";
import {
CONTROL_CODES,
CONTROL_CODE_DESCRIPTIONS,
CONTROL_CODE_EXAMPLES,
CONTROL_CODES,
} from "../types/control-codes.js";
import {
SESSION_ID_SYMBOL,
Expand Down
10 changes: 5 additions & 5 deletions packages/mcp-pty/src/server/handlers/tools.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { z } from "zod";
import { resolveControlCode } from "../../types/control-codes";
import { normalizeWorkingDirectory } from "../../utils";
import {
type KillPtyInputSchema,
type ListPtyInputSchema,
type ReadPtyInputSchema,
type StartPtyInputSchema,
import type {
KillPtyInputSchema,
ListPtyInputSchema,
ReadPtyInputSchema,
StartPtyInputSchema,
WriteInputSchema,
} from "../schemas";
import type { HandlerContext, ToolResult } from "../types";
Expand Down
48 changes: 21 additions & 27 deletions packages/mcp-pty/src/transports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { sessionManager } from "@pkgs/session-manager";
import { createLogger } from "@pkgs/logger";
import { toFetchResponse, toReqRes } from "fetch-to-node";
import { Hono } from "hono";
import { cors } from "hono/cors";
Expand All @@ -11,8 +10,6 @@ import { bindSessionToServerResources } from "../resources";
import { bindSessionToServer } from "../tools";
import { logError, logServer } from "../utils";

const transportLogger = createLogger("http-transport");

/**
* MCP Streamable HTTP Error Handling Strategy
*
Expand Down Expand Up @@ -202,13 +199,13 @@ export const startHttpServer = async (
// Reconnect to existing active session
// Don't immediately connect - let deferred initialization happen
sessionId = sessionHeader;
const server = serverFactory();
const transport = createHttpTransport(sessionId);
const newServer = serverFactory();
const newTransport = createHttpTransport(sessionId);

initializeSessionBindings(server, sessionId);
// Defer server.connect() to first request to avoid transport reuse after reconnection
initializeSessionBindings(newServer, sessionId);
// DON'T call server.connect() yet - let it happen via handleRequest()

session = { server, transport };
session = { server: newServer, transport: newTransport };
sessions.set(sessionId, session);
logServer(`Prepared reconnection for session: ${sessionId}`);
}
Expand All @@ -220,13 +217,13 @@ export const startHttpServer = async (
);

const newSessionId = sessionManager.createSession();
const server = serverFactory();
const transport = createHttpTransport(newSessionId);
const newServer = serverFactory();
const newTransport = createHttpTransport(newSessionId);

initializeSessionBindings(server, newSessionId);
// Defer server.connect() to first request to prevent transport state issues
initializeSessionBindings(newServer, newSessionId);
// DON'T call server.connect() yet - let it happen when client sends first request with new ID

const newSession = { server, transport };
const newSession = { server: newServer, transport: newTransport };
sessions.set(newSessionId, newSession);

logServer(`Created new session for reconnection: ${newSessionId}`);
Expand All @@ -242,7 +239,7 @@ export const startHttpServer = async (
const transport = createHttpTransport(sessionId);

initializeSessionBindings(server, sessionId);
// Defer server.connect() to first request to prevent unnecessary connections
// DON'T call server.connect() yet - let it happen via handleRequest()

session = { server, transport };
sessions.set(sessionId, session);
Expand Down Expand Up @@ -278,13 +275,13 @@ export const startHttpServer = async (
}
// Session not found, create new session ID for client to use
const newSessionId = sessionManager.createSession();
const server = serverFactory();
const transport = createHttpTransport(newSessionId);
const newServer = serverFactory();
const newTransport = createHttpTransport(newSessionId);

initializeSessionBindings(server, newSessionId);
// Defer server.connect() to first request to prevent transport state issues
initializeSessionBindings(newServer, newSessionId);
// DON'T call server.connect() yet - let it happen when client sends first request with new ID

const newSession = { server, transport };
const newSession = { server: newServer, transport: newTransport };
sessions.set(newSessionId, newSession);

logServer(
Expand All @@ -296,14 +293,11 @@ export const startHttpServer = async (
}

// Log request for debugging
transportLogger.debug(
`[${currentSessionId}] ${c.req.method} /mcp - headers:`,
{
"mcp-session-id": c.req.header("mcp-session-id"),
"content-type": c.req.header("content-type"),
accept: c.req.header("accept"),
},
);
console.log(`[${currentSessionId}] ${c.req.method} /mcp - headers:`, {
"mcp-session-id": c.req.header("mcp-session-id"),
"content-type": c.req.header("content-type"),
accept: c.req.header("accept"),
});

// For POST/PUT requests, use raw request (do NOT read body with c.req.text())
// The transport layer needs the original stream to handle JSON-RPC parsing
Expand Down
21 changes: 21 additions & 0 deletions packages/normalize-commands/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,24 @@ test.each(testCases)(
expect(result).toBe(expected);
},
);

// ============================================================================
// Security Tests
// ============================================================================

test("should block sh -c bypass for dangerous commands", () => {
expect(() => normalizeCommand('sh -c "rm -rf /"')).toThrow(
/Dangerous command pattern detected: rm -rf/,
);
});

test("should block sh -c bypass for privilege escalation", () => {
expect(() => normalizeCommand('sh -c "sudo rm -rf /tmp"')).toThrow(
/Privilege escalation command detected: sudo/,
);
});

test("should allow safe sh -c commands", () => {
const result = normalizeCommand('sh -c "echo hello"');
expect(result).toBe('{"command":"sh","args":["-c","echo hello"]}');
});
4 changes: 1 addition & 3 deletions packages/normalize-commands/src/__tests__/security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ test("should block dd to block device", () => {
);
});

// Fork bomb detection is complex and may not catch all variants
// This is an edge case that bash-parser handles but doesn't match the regex
test.skip("should block fork bomb", () => {
test("should block fork bomb", () => {
expect(() => normalizeCommand(":(){ :|:& };:")).toThrow(
/Dangerous command pattern detected/,
);
Expand Down
Loading