diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 6a27694d..f39b9a25 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642195b5c3ad3664e5088c2 # v6 with: node-version: 24 registry-url: https://registry.npmjs.org @@ -61,20 +61,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v4 - name: Log in to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push by digest id: build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v7 with: context: . platforms: ${{ matrix.platform }} @@ -89,7 +89,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v6 with: name: digests-${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} path: /tmp/digests/* @@ -108,17 +108,17 @@ jobs: steps: - name: Download digests - uses: actions/download-artifact@v6 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v6 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v4 - name: Log in to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -138,7 +138,7 @@ jobs: echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - name: Install cosign - uses: sigstore/cosign-installer@v4.1.0 + uses: sigstore/cosign-installer@3454372be43e8dfc343a2f44a3a73ed6db44bcb4 # v4.1.0 - name: Sign Docker image run: cosign sign --yes "keygraph/shannon@${{ steps.inspect.outputs.digest }}" @@ -161,13 +161,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 - name: Configure npm registry - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642195b5c3ad3664e5088c2 # v6 with: node-version: 24 registry-url: https://registry.npmjs.org diff --git a/.github/workflows/rollback-beta.yml b/.github/workflows/rollback-beta.yml index 00148936..f4e15a88 100644 --- a/.github/workflows/rollback-beta.yml +++ b/.github/workflows/rollback-beta.yml @@ -38,7 +38,7 @@ jobs: echo "version=$VERSION" >> "$GITHUB_OUTPUT" - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@49933ea5288caeca8642195b5c3ad3664e5088c2 # v6 with: node-version: 24 registry-url: https://registry.npmjs.org diff --git a/Dockerfile b/Dockerfile index a78a2108..ae96fdbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ # Uses Chainguard Wolfi for minimal attack surface and supply chain security # Builder stage - Install tools and dependencies -FROM cgr.dev/chainguard/wolfi-base:latest AS builder +FROM cgr.dev/chainguard/wolfi-base:20250319 AS builder # Install system dependencies available in Wolfi RUN apk update && apk add --no-cache \ @@ -38,7 +38,7 @@ ENV CGO_ENABLED=1 RUN mkdir -p $GOPATH/bin # Install Go-based security tools -RUN go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest +RUN go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@v2.6.8 # Install WhatWeb from GitHub (Ruby-based tool) RUN git clone --depth 1 https://github.com/urbanadventurer/WhatWeb.git /opt/whatweb && \ chmod +x /opt/whatweb/whatweb && \ @@ -51,7 +51,7 @@ RUN git clone --depth 1 https://github.com/urbanadventurer/WhatWeb.git /opt/what RUN pip3 install --no-cache-dir schemathesis # Runtime stage - Minimal production image -FROM cgr.dev/chainguard/wolfi-base:latest AS runtime +FROM cgr.dev/chainguard/wolfi-base:20250319 AS runtime # Install only runtime dependencies USER root @@ -132,10 +132,10 @@ RUN npm install -g @anthropic-ai/claude-code # Create directories for session data and ensure proper permissions RUN mkdir -p /app/sessions /app/deliverables /app/repos /app/configs && \ mkdir -p /tmp/.cache /tmp/.config /tmp/.npm && \ - chmod 777 /app && \ - chmod 777 /tmp/.cache && \ - chmod 777 /tmp/.config && \ - chmod 777 /tmp/.npm && \ + chmod 750 /app && \ + chmod 750 /tmp/.cache && \ + chmod 750 /tmp/.config && \ + chmod 750 /tmp/.npm && \ chown -R pentest:pentest /app # Switch to non-root user @@ -155,7 +155,7 @@ ENV XDG_CONFIG_HOME=/tmp/.config # Configure Git identity and trust all directories RUN git config --global user.email "agent@localhost" && \ git config --global user.name "Pentest Agent" && \ - git config --global --add safe.directory '*' + git config --global --add safe.directory /app && git config --global --add safe.directory /repos # Set entrypoint ENTRYPOINT ["node", "dist/shannon.js"] diff --git a/configs/example-config.yaml b/configs/example-config.yaml index b78d9ba9..a68b70e3 100644 --- a/configs/example-config.yaml +++ b/configs/example-config.yaml @@ -5,9 +5,9 @@ authentication: login_type: form # Options: 'form' or 'sso' login_url: "https://example.com/login" credentials: - username: "testuser" - password: "testpassword" - totp_secret: "JBSWY3DPEHPK3PXP" # Optional TOTP secret for 2FA + username: "CHANGE_ME_USERNAME" # REQUIRED: Replace with your actual username + password: "CHANGE_ME_PASSWORD" # REQUIRED: Replace with your actual password + totp_secret: "CHANGE_ME_SECRET" # Optional: Replace with your base32 TOTP secret for 2FA # Natural language instructions for login flow login_flow: diff --git a/configs/router-config.json b/configs/router-config.json index cf57b1e9..0a324209 100644 --- a/configs/router-config.json +++ b/configs/router-config.json @@ -1,10 +1,10 @@ { "HOST": "0.0.0.0", - "APIKEY": "shannon-router-key", + "APIKEY": "$SHANNON_ROUTER_APIKEY", "LOG": true, - "LOG_LEVEL": "info", + "LOG_LEVEL": "warn", "NON_INTERACTIVE_MODE": true, - "API_TIMEOUT_MS": 600000, + "API_TIMEOUT_MS": 60000, "Providers": [ { "name": "openai", diff --git a/docker-compose.yml b/docker-compose.yml index eede388e..dbe2e134 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: temporal: - image: temporalio/temporal:latest + image: temporalio/temporal:1.26.2 command: ["server", "start-dev", "--db-filename", "/home/temporal/temporal.db", "--ip", "0.0.0.0"] ports: - "127.0.0.1:7233:7233" # gRPC @@ -46,9 +46,15 @@ services: - ./credentials:/app/credentials:ro - ./repos:/repos - ${BENCHMARKS_BASE:-.}:/benchmarks - shm_size: 2gb - security_opt: - - seccomp:unconfined + shm_size: 512mb + deploy: + resources: + limits: + cpus: '4' + memory: 8G + reservations: + cpus: '1' + memory: 2G # Optional: claude-code-router for multi-model support # Start with: ROUTER=true ./shannon start ... diff --git a/mcp-server/src/tools/generate-totp.ts b/mcp-server/src/tools/generate-totp.ts index 7f89da72..864ce5a6 100644 --- a/mcp-server/src/tools/generate-totp.ts +++ b/mcp-server/src/tools/generate-totp.ts @@ -26,6 +26,7 @@ export const GenerateTotpInputSchema = z.object({ secret: z .string() .min(1) + .max(256, 'Secret too long') .regex(/^[A-Z2-7]+$/i, 'Must be base32-encoded') .describe('Base32-encoded TOTP secret'), }); diff --git a/mcp-server/src/tools/save-deliverable.ts b/mcp-server/src/tools/save-deliverable.ts index db038ce2..be368afa 100644 --- a/mcp-server/src/tools/save-deliverable.ts +++ b/mcp-server/src/tools/save-deliverable.ts @@ -40,8 +40,21 @@ export type SaveDeliverableInput = z.infer; * Prevents path traversal attacks (e.g., ../../../etc/passwd). */ function isPathContained(basePath: string, targetPath: string): boolean { - const resolvedBase = path.resolve(basePath); - const resolvedTarget = path.resolve(targetPath); + let resolvedBase: string; + let resolvedTarget: string; + try { + resolvedBase = fs.realpathSync(path.resolve(basePath)); + } catch { + resolvedBase = path.resolve(basePath); + } + try { + // For new files, resolve the parent directory through symlinks + const parentDir = path.dirname(path.resolve(targetPath)); + const parentReal = fs.realpathSync(parentDir); + resolvedTarget = path.join(parentReal, path.basename(targetPath)); + } catch { + resolvedTarget = path.resolve(targetPath); + } return resolvedTarget === resolvedBase || resolvedTarget.startsWith(resolvedBase + path.sep); } diff --git a/mcp-server/src/utils/file-operations.ts b/mcp-server/src/utils/file-operations.ts index 150c38d4..ef2dc1b4 100644 --- a/mcp-server/src/utils/file-operations.ts +++ b/mcp-server/src/utils/file-operations.ts @@ -11,8 +11,9 @@ * Ported from tools/save_deliverable.js (lines 117-130). */ -import { writeFileSync, mkdirSync } from 'fs'; -import { join } from 'path'; +import { writeFileSync, mkdirSync, renameSync, unlinkSync } from 'fs'; +import { join, dirname } from 'path'; +import { randomBytes } from 'crypto'; /** * Save deliverable file to deliverables/ directory @@ -32,8 +33,16 @@ export function saveDeliverableFile(targetDir: string, filename: string, content throw new Error(`Cannot create deliverables directory at ${deliverablesDir}`); } - // Write file (atomic write - single operation) - writeFileSync(filepath, content, 'utf8'); + // Atomic write: write to temp file then rename + const tempPath = join(deliverablesDir, `.tmp-${randomBytes(8).toString('hex')}`); + try { + writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o644 }); + renameSync(tempPath, filepath); + } catch (err) { + // Clean up temp file on failure + try { unlinkSync(tempPath); } catch { /* ignore */ } + throw err; + } return filepath; } diff --git a/mcp-server/src/validation/queue-validator.ts b/mcp-server/src/validation/queue-validator.ts index c0439340..370011a2 100644 --- a/mcp-server/src/validation/queue-validator.ts +++ b/mcp-server/src/validation/queue-validator.ts @@ -25,7 +25,7 @@ export interface ValidationResult { */ export function validateQueueJson(content: string): ValidationResult { try { - const parsed = JSON.parse(content) as unknown; + const parsed = JSON.parse(content, (key, value) => key === '__proto__' || key === 'constructor' ? undefined : value) as unknown; // Type guard for the parsed result if (typeof parsed !== 'object' || parsed === null) { diff --git a/package-lock.json b/package-lock.json index 9d2b9807..2eba6232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -606,7 +606,6 @@ "integrity": "sha512-T8keoJjXaSUoVBCIjgL6wAnhADIb09GOELzKg10CjNg+vLX48P93SME6jTfte9MZIm5m+Il57H3rTSk/0kzDUw==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" @@ -1179,7 +1178,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1200,11 +1198,10 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -1375,7 +1372,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1856,9 +1852,9 @@ } }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2021,15 +2017,6 @@ "node": ">=12.0.0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2057,26 +2044,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -2102,15 +2069,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/source-map": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", @@ -2260,15 +2218,14 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -2351,8 +2308,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/type-fest": { "version": "4.41.0", @@ -2455,7 +2411,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -2628,9 +2583,9 @@ } }, "node_modules/zx": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.1.tgz", - "integrity": "sha512-qvsKBnvWHstHKCluKPlEgI/D3+mdiQyMoSSeFR8IX/aXzWIas5A297KxKgPJhuPXdrR6ma0Jzx43+GQ/8sqbrw==", + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", "license": "Apache-2.0", "bin": { "zx": "build/cli.js" diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index 3f7fd0cb..d4ae2cb2 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -270,14 +270,18 @@ export async function runClaudePrompt( model: resolveModel(modelTier), maxTurns: 10_000, cwd: sourceDir, - permissionMode: 'bypassPermissions' as const, - allowDangerouslySkipPermissions: true, + permissionMode: 'allowedTools' as const, + allowedTools: [ + 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', + 'mcp__shannon-helper__save_deliverable', + 'mcp__shannon-helper__generate_totp', + ], mcpServers, env: sdkEnv, }; if (!execContext.useCleanOutput) { - logger.info(`SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=BYPASS`); + logger.info(`SDK Options: maxTurns=${options.maxTurns}, cwd=${sourceDir}, permissions=ALLOWLIST`); } let turnCount = 0; diff --git a/src/services/prompt-manager.ts b/src/services/prompt-manager.ts index b852c73e..5c1bf6dd 100644 --- a/src/services/prompt-manager.ts +++ b/src/services/prompt-manager.ts @@ -21,6 +21,21 @@ interface IncludeReplacement { content: string; } +/** + * Sanitize user-provided values before prompt interpolation to prevent prompt injection. + * Strips characters that could break out of template context. + */ +function sanitizeForPrompt(value: string): string { + return value + .replace(/[<>]/g, '') // Remove XML/HTML tag chars + .replace(//g, '') // Remove HTML comments + .replace(/\{\{/g, '') // Remove template delimiters + .replace(/\}\}/g, '') + .replace(/\n/g, ' ') // Flatten to single line + .trim() + .slice(0, 256); // Limit length +} + // Pure function: Build complete login instructions from config async function buildLoginInstructions(authentication: Authentication, logger: ActivityLogger): Promise { try { @@ -67,13 +82,14 @@ async function buildLoginInstructions(authentication: Authentication, logger: Ac if (authentication.credentials) { if (authentication.credentials.username) { - userInstructions = userInstructions.replace(/\$username/g, authentication.credentials.username); + userInstructions = userInstructions.replace(/\$username/g, sanitizeForPrompt(authentication.credentials.username)); } if (authentication.credentials.password) { - userInstructions = userInstructions.replace(/\$password/g, authentication.credentials.password); + userInstructions = userInstructions.replace(/\$password/g, sanitizeForPrompt(authentication.credentials.password)); } if (authentication.credentials.totp_secret) { - userInstructions = userInstructions.replace(/\$totp/g, `generated TOTP code using secret "${authentication.credentials.totp_secret}"`); + const sanitizedSecret = sanitizeForPrompt(authentication.credentials.totp_secret); + userInstructions = userInstructions.replace(/\$totp/g, `generated TOTP code using secret "${sanitizedSecret}"`); } } @@ -81,7 +97,7 @@ async function buildLoginInstructions(authentication: Authentication, logger: Ac // 5. Replace TOTP secret placeholder if present in template if (authentication.credentials?.totp_secret) { - loginInstructions = loginInstructions.replace(/{{totp_secret}}/g, authentication.credentials.totp_secret); + loginInstructions = loginInstructions.replace(/{{totp_secret}}/g, sanitizeForPrompt(authentication.credentials.totp_secret)); } return loginInstructions; diff --git a/src/services/queue-validation.ts b/src/services/queue-validation.ts index dde8666c..c5eb07f1 100644 --- a/src/services/queue-validation.ts +++ b/src/services/queue-validation.ts @@ -204,7 +204,7 @@ const validateExistenceRules = ( // Pure function to validate queue structure const validateQueueStructure = (content: string): QueueValidationResult => { try { - const parsed = JSON.parse(content) as unknown; + const parsed = JSON.parse(content, (key, value) => key === '__proto__' || key === 'constructor' ? undefined : value) as unknown; const isValid = typeof parsed === 'object' && parsed !== null && diff --git a/src/temporal/workspaces.ts b/src/temporal/workspaces.ts index 62d6b299..0202c438 100644 --- a/src/temporal/workspaces.ts +++ b/src/temporal/workspaces.ts @@ -84,7 +84,7 @@ async function listWorkspaces(): Promise { const sessionPath = path.join(auditDir, entry, 'session.json'); try { const content = await fs.readFile(sessionPath, 'utf8'); - const data = JSON.parse(content) as SessionJson; + const data = JSON.parse(content, (key, value) => key === '__proto__' || key === 'constructor' ? undefined : value) as SessionJson; workspaces.push({ name: entry, diff --git a/src/utils/file-io.ts b/src/utils/file-io.ts index 0f35c83f..dcc5171c 100644 --- a/src/utils/file-io.ts +++ b/src/utils/file-io.ts @@ -55,9 +55,16 @@ export async function atomicWrite(filePath: string, data: object | string): Prom /** * Read and parse JSON file */ +export function safeJsonParse(text: string): T { + return JSON.parse(text, (key, value) => { + if (key === '__proto__' || key === 'constructor') return undefined; + return value; + }) as T; +} + export async function readJson(filePath: string): Promise { const content = await fs.readFile(filePath, 'utf8'); - return JSON.parse(content) as T; + return safeJsonParse(content); } /**