diff --git a/.jules/sentinel.md b/.jules/sentinel.md index a6c92980..d3fc608b 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -126,3 +126,8 @@ Error messages should never include raw values from sensitive sources like envir **Prevention:** 1. Avoid including raw values in error messages when the source is potentially sensitive (env vars, auth headers). 2. Use generic error messages for validation failures of sensitive data. + +## 2024-05-08 - [Path Traversal / URL Injection in encodePathSegment] +**Vulnerability:** In `mcp-server.ts`, dot characters (`.`) injected into the URL paths were not encoded by `encodeURIComponent`. +**Learning:** `encodeURIComponent` ignores `.`. In addition, using a ternary check like `val.includes('/') ? encodeURIComponent(val) : val;` completely bypassed the encoding check if the malicious string was exactly `..` (because it does not contain a slash). +**Prevention:** Always encode all URL path values without conditional string presence checks (e.g. unconditionally calling `encodeURIComponent(val).replace(/\./g, '%2E')` handles both directory traversal payloads and standard pathing safely). diff --git a/src/mcp/mcp-server.ts b/src/mcp/mcp-server.ts index e86823cf..2c73eaed 100644 --- a/src/mcp/mcp-server.ts +++ b/src/mcp/mcp-server.ts @@ -1205,7 +1205,7 @@ export class MCPServer { */ private encodePathSegment(value: unknown): string { const val = String(value); - return val.includes('/') ? encodeURIComponent(val) : val; + return encodeURIComponent(val).replace(/\./g, '%2E'); } /** diff --git a/src/tooling/composite-executor-security.test.ts b/src/tooling/composite-executor-security.test.ts index 4f7f1ca3..97a3b0b6 100644 --- a/src/tooling/composite-executor-security.test.ts +++ b/src/tooling/composite-executor-security.test.ts @@ -67,7 +67,7 @@ describe('CompositeExecutor Security', () => { // Fixed behavior: path contains encoded "%2E%2E" // Note: encodeURIComponent('../admin/secrets') => ..%2Fadmin%2Fsecrets // The slashes inside the injected value are encoded, preventing directory traversal - const expectedFixedPath = '/users/..%2Fadmin%2Fsecrets/profile'; + const expectedFixedPath = '/users/%2E%2E%2Fadmin%2Fsecrets/profile'; expect(capturedPaths[0]).toBe(expectedFixedPath); }); diff --git a/src/tooling/composite-executor.ts b/src/tooling/composite-executor.ts index 34e467a3..9f4f6d01 100644 --- a/src/tooling/composite-executor.ts +++ b/src/tooling/composite-executor.ts @@ -171,14 +171,14 @@ export class CompositeExecutor { return template.replace(/\{(\w+)\}/g, (_, key) => { // Try direct match first if (args[key] !== undefined) { - return encodeURIComponent(String(args[key])); + return encodeURIComponent(String(args[key])).replace(/\./g, '%2E'); } // Try aliases from profile const possibleAliases = this.parameterAliases[key] || []; for (const alias of possibleAliases) { if (args[alias] !== undefined) { - return encodeURIComponent(String(args[alias])); + return encodeURIComponent(String(args[alias])).replace(/\./g, '%2E'); } }