Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,14 @@ 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.

## 2026-04-15 - [CRITICAL] Path Traversal Due To Incomplete URL Encoding

**Vulnerability:**
Both `CompositeExecutor` and `MCPServer` were incorrectly encoding URL path segments, leaving dots (`.`) unencoded (`encodeURIComponent` does not encode dots). This allowed path traversal sequences like `..` to be passed via parameter injection.

**Learning:**
When injecting user-controlled path parameters, relying solely on JavaScript's built-in `encodeURIComponent` is insufficient if the downstream components decode `%2E` or interpret raw `.` paths.

**Prevention:**
1. Explicitly encode dot characters `.replace(/\./g, '%2E')` whenever constructing path segments containing user input.
2 changes: 1 addition & 1 deletion src/mcp/mcp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1107,7 +1107,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');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pre-encoded path params when interpolating URLs

This change now percent-encodes every path value unconditionally, so inputs that are already URL-encoded (for example group%2Fproject, which is explicitly supported in profile parameter docs) are transformed to group%252Fproject. That breaks existing callers that pass encoded project/file paths, because backends like GitLab will receive the wrong identifier and return not found/invalid path errors. The previous behavior intentionally left already-encoded values intact when they did not contain raw /, so this is a backward-incompatible regression in core request routing.

Useful? React with πŸ‘Β / πŸ‘Ž.

}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/tooling/composite-executor-security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ describe('CompositeExecutor Security', () => {

// Vulnerable behavior: path contains raw ".."
// 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';
// Note: encodeURIComponent('../admin/secrets').replace(/\./g, '%2E') => %2E%2E%2Fadmin%2Fsecrets
// The slashes and dots inside the injected value are encoded, preventing directory traversal
const expectedFixedPath = '/users/%2E%2E%2Fadmin%2Fsecrets/profile';

expect(capturedPaths[0]).toBe(expectedFixedPath);
});
Expand Down
4 changes: 2 additions & 2 deletions src/tooling/composite-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}

Expand Down
Loading