Skip to content

fix(http): preserve caller's URL encoding in path params#1600

Open
hugo-ccabral wants to merge 1 commit into
mainfrom
fix/http-client-preserve-encoding
Open

fix(http): preserve caller's URL encoding in path params#1600
hugo-ccabral wants to merge 1 commit into
mainfrom
fix/http-client-preserve-encoding

Conversation

@hugo-ccabral

@hugo-ccabral hugo-ccabral commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Regression introduced by #1525 (commit 3ff0d90): when admin tries to fetch a file whose name contains a space (or any pre-encoded character), the path on the wire arrived wrong, causing 404s.

normalizePathParam decoded the input in a loop until fixed point, then encodePathSegment re-encoded only once — collapsing the caller's encoding level by one. The same path also encoded / as %2F, breaking wildcard route segments (later partially fixed in #1528 / #1533).

Repro: filepath = "/.deco/blocks/pages-Home%2520ETC-330706.json"

  • Before (buggy): GET /fs/file/.deco%2Fblocks%2Fpages-Home%20ETC-330706.json
  • After (this PR): GET /fs/file/.deco/blocks/pages-Home%2520ETC-330706.json ✓ (matches pre-Tavano/fix path traversal #1525 behaviour)

Fix

  • Validate against a fully-decoded copy of the value (so encoded path traversal — including double-encoded — is still caught).
  • Return the original input verbatim; drop the re-encode step.
  • Drop the now-unused encodePathSegment helper.

This restores the pre-#1525 pass-through behaviour while preserving the security checks #1525 introduced: ../, ..\, absolute paths, and null bytes.

Security

All attacks still blocked by validating the fully-decoded form:

  • ../../etc/passwd../ literal match
  • ..%2F..%2Fpasswd → multi-decode → ../../passwd → caught
  • ..%252F..%252Fpasswd → decoded to fixed point → caught
  • /etc/passwd, %2Fetc%2Fpasswd → absolute path check on decoded form
  • foo\0bar → null-byte check on both raw and decoded
  • Backslash variants (..\) → checked explicitly

One intentional relaxation: dropped the segment === "." rejection (a bare . segment is harmless; only .. is a traversal vector). Happy to add it back if preferred.

Test plan

  • Admin can read /.deco/blocks/pages-Home%2520ETC-330706.json via the daemon fs/read loader
  • Existing HTTP-client consumers (vtex, vnda, smarthint, konfidency, etc.) unaffected
  • Path traversal attempts (../, ..%2F, ..%252F, leading /) still rejected with HTTP 400

🤖 Generated with Claude Code


Summary by cubic

Preserves caller URL encoding in HTTP path params to fix 404s and broken wildcard routes for pre-encoded filenames (e.g. %2520). Restores pre-#1525 pass-through while keeping traversal and absolute-path protections.

  • Bug Fixes
    • Validate against a fully-decoded copy; return the original value verbatim (no re-encode).
    • Remove encodePathSegment; stop encoding / as %2F in wildcard params.
    • Continue rejecting ../, ..\, leading / or \, and null bytes (checked on raw and decoded).
    • Allow . segments again (only .. is blocked).

Written for commit 8fdc9bd. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced path parameter validation to properly handle and reject invalid parameters with appropriate error responses.

Review Change Stack

normalizePathParam decoded the input repeatedly while the path-builder
re-encoded only once, collapsing the caller's intended encoding level
(e.g. "%2520" became "%20"). The wildcard interpolation also encoded
"/" as "%2F", breaking wildcard route segments. Together this broke
admin's daemon fs reads for filenames containing %-escaped characters
or spaces.

Validate against a fully-decoded copy (so encoded path-traversal is
still caught, including double-encoded variants) but return the
original input verbatim and skip the re-encoding step. This restores
the pre-#1525 pass-through behaviour while keeping the traversal,
absolute-path, and null-byte protections introduced there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 0.153.1 update
  • 🎉 for Minor 0.154.0 update
  • 🚀 for Major 1.0.0 update

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors HTTP path parameter validation in utils/http.ts. The normalizePathParam function is rewritten to validate decoded parameters for path traversal, absolute paths, and null bytes while returning original input strings verbatim. The route template substitution in createHttpClient is updated to validate both scalar and array parameters through the new validation function and insert them directly without additional encoding.

Changes

Path Parameter Validation Refactor

Layer / File(s) Summary
Path parameter validation function
utils/http.ts
normalizePathParam is rewritten to repeatedly decode and validate path parameters against traversal (../, ..\\), absolute paths, null bytes, and .. segments, then return the original input verbatim instead of a normalized value.
Route template parameter handling
utils/http.ts
Route template parameter substitution in createHttpClient validates scalar and array template parameters via normalizePathParam and inserts the returned verbatim strings directly into compiled paths; validation errors map to HttpError(400, "Invalid parameter '<name>'").

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • deco-cx/apps#1525: Overlaps in modifying utils/http.ts path parameter validation and createHttpClient template parameter handling, though with different sanitization approaches.

Poem

🐰 A path walks in, decoded and checked with care,
No ../ tricks shall slip past the guard,
Verbatim strings now roam the template air,
Secured and honest, no encoding façade!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main fix: preserving caller's URL encoding in path parameters, which is the central change in this PR.
Description check ✅ Passed The PR description provides a comprehensive explanation of the regression, the fix, security considerations, and a detailed test plan, but does not strictly follow the repository's required template structure (missing Issue Link section with #ISSUE_NUMBER and Loom Video/Demonstration Link sections).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/http-client-preserve-encoding

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@utils/http.ts`:
- Around line 119-121: The catch that swallows decodeURIComponent errors should
instead treat undecodable input as invalid: in the decode block (where
decodeURIComponent is called and the local variable decoded is set) do not leave
the original encoded string in place on error—return/throw a validation failure
so the caller (e.g., the path validation logic around decoded) rejects the
input; update that catch to mark the input as undecodable (for example by
returning false or throwing a specific error) so inputs like `%2e%2e%2f%GGfoo`
cannot bypass the `../` check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 142892c7-1a35-42ce-a29e-f2b0925c7840

📥 Commits

Reviewing files that changed from the base of the PR and between 288fa2e and 8fdc9bd.

📒 Files selected for processing (1)
  • utils/http.ts

Comment thread utils/http.ts
Comment on lines 119 to 121
} catch {
// If decode fails, keep the last successful decoded value
// Do not reset to original string as that would bypass security checks
// Partial decode is acceptable for validation purposes.
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider rejecting inputs that fail to fully decode.

If decodeURIComponent throws on malformed sequences (e.g., %GG), validation proceeds against the original encoded string. An input like %2e%2e%2f%GGfoo would bypass the ../ check since decoded remains unchanged at %2e%2e%2f%GGfoo.

While standard URL decoders should also fail on such input, some non-conforming servers or proxies may decode valid sequences while ignoring invalid ones. Rejecting undecodable input would close this edge case.

🛡️ Suggested hardening
   } catch {
-    // Partial decode is acceptable for validation purposes.
+    throw new Error(
+      `Invalid URL encoding in parameter '${paramName}'`,
+    );
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/http.ts` around lines 119 - 121, The catch that swallows
decodeURIComponent errors should instead treat undecodable input as invalid: in
the decode block (where decodeURIComponent is called and the local variable
decoded is set) do not leave the original encoded string in place on
error—return/throw a validation failure so the caller (e.g., the path validation
logic around decoded) rejects the input; update that catch to mark the input as
undecodable (for example by returning false or throwing a specific error) so
inputs like `%2e%2e%2f%GGfoo` cannot bypass the `../` check.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="utils/http.ts">

<violation number="1" location="utils/http.ts:120">
P2: Security: When `decodeURIComponent` throws on malformed sequences (e.g., `%2e%2e%2f%GGfoo`), the entire decode fails and `decoded` remains equal to the original encoded string. The subsequent `includes("../")` check won't detect the traversal because it's still percent-encoded. If a downstream proxy partially decodes valid sequences while ignoring invalid ones, this could allow a traversal bypass. Consider rejecting inputs that fail to fully decode rather than silently proceeding with the un-decoded value.</violation>

<violation number="2" location="utils/http.ts:137">
P2: Allowing `.` path segments lets `URL()` collapse the path (e.g. `/users/.` -> `/users/`), so a literal path param can resolve to a different endpoint.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread utils/http.ts
const segments = decoded.replace(/\\/g, "/").split("/");
for (const segment of segments) {
if (segment === ".." || segment === ".") {
if (segment === "..") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Allowing . path segments lets URL() collapse the path (e.g. /users/. -> /users/), so a literal path param can resolve to a different endpoint.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At utils/http.ts, line 137:

<comment>Allowing `.` path segments lets `URL()` collapse the path (e.g. `/users/.` -> `/users/`), so a literal path param can resolve to a different endpoint.</comment>

<file context>
@@ -97,78 +97,57 @@ export interface HttpClientOptions {
+  const segments = decoded.replace(/\\/g, "/").split("/");
   for (const segment of segments) {
-    if (segment === ".." || segment === ".") {
+    if (segment === "..") {
       throw new Error(
         `Invalid path segment in parameter '${paramName}'`,
</file context>
Suggested change
if (segment === "..") {
if (segment === ".." || segment === ".") {

Comment thread utils/http.ts
} catch {
// If decode fails, keep the last successful decoded value
// Do not reset to original string as that would bypass security checks
// Partial decode is acceptable for validation purposes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Security: When decodeURIComponent throws on malformed sequences (e.g., %2e%2e%2f%GGfoo), the entire decode fails and decoded remains equal to the original encoded string. The subsequent includes("../") check won't detect the traversal because it's still percent-encoded. If a downstream proxy partially decodes valid sequences while ignoring invalid ones, this could allow a traversal bypass. Consider rejecting inputs that fail to fully decode rather than silently proceeding with the un-decoded value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At utils/http.ts, line 120:

<comment>Security: When `decodeURIComponent` throws on malformed sequences (e.g., `%2e%2e%2f%GGfoo`), the entire decode fails and `decoded` remains equal to the original encoded string. The subsequent `includes("../")` check won't detect the traversal because it's still percent-encoded. If a downstream proxy partially decodes valid sequences while ignoring invalid ones, this could allow a traversal bypass. Consider rejecting inputs that fail to fully decode rather than silently proceeding with the un-decoded value.</comment>

<file context>
@@ -97,78 +97,57 @@ export interface HttpClientOptions {
   } catch {
-    // If decode fails, keep the last successful decoded value
-    // Do not reset to original string as that would bypass security checks
+    // Partial decode is acceptable for validation purposes.
   }
 
</file context>
Suggested change
// Partial decode is acceptable for validation purposes.
throw new Error(
`Invalid URL encoding in parameter '${paramName}'`,
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant