Skip to content

fix(local-http): bound nested percent decoding under security owner - #1635

Draft
seonghobae wants to merge 4 commits into
autoresearch/frontend-sec-bumpfrom
autoresearch/local-http-nested-decoding
Draft

fix(local-http): bound nested percent decoding under security owner#1635
seonghobae wants to merge 4 commits into
autoresearch/frontend-sec-bumpfrom
autoresearch/local-http-nested-decoding

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Current authority — 2026-09-10

  • canonical prerequisite: #1623@17a7618eda2b212b691f08fa936e042b34258fc9
  • first source-order RED: 763854fe229ca89e39497d00dea6e2b4848ee310
  • first causal fix: ff7f85a54f2027fc7d35748c916aa066e8072739
  • review RED: 709cb1b4bf05fa549be66c0e448e32902dbeffcc
  • exact head: 558c830651b64c2434cb64791a804f8059da6773
  • lifecycle: Draft / canonical local HTTP nested-decoding hardening successor / do not merge yet
  • effective delta: exactly 2 files — backend/core/local_http.py, backend/tests/test_local_http.py.

Generated PR #1634 identified a real helper-level validation gap but was based directly on stale protected develop, omitted a retained regression, carried an unrelated generated security-history note, and overstated the live exploit as CRITICAL SSRF/arbitrary local endpoint access. This successor keeps only the verified causal delta and inherits the current frontend dependency-security owner rather than reproducing its Trivy RED.

The helper is explicitly for local HTTP smoke/live-test request targets. Static call-graph inspection found consumers passing the original validated target directly to HTTPConnection.request/HTTPSConnection.request and found no explicit downstream decode. This remains defense-in-depth/future-proofing, not a verified production SSRF.

TDD/review lineage is explicit. RED 763854f... adds nested traversal/backslash/control and excessive-depth cases on unchanged #1623 source. ff7f85a... implements bounded decode-until-stable semantics. CodeRabbit then found a valid P3 contract hole: a valid outer encoding could reveal a malformed nested escape (%2525GG -> %25GG -> %GG, or %252 -> %2) that urllib.parse.unquote() leaves stable. RED 709cb1b... adds those cases while also locking preservation of a safe encoded literal percent (/api/literal%25). Exact head 558c830... adds a decoded-layer malformed-escape check that permits a terminal literal % but rejects a percent followed by characters that are not a complete hex escape before continuing recursive decoding. The ten-round ceiling and original-path return semantics are unchanged.

Current review and execution evidence

CodeRabbit re-reviewed the exact base/head 17a7618... -> 558c830... after the earlier transient capacity limit, selected exactly the two owned files, generated no actionable comments, and submitted formal APPROVED review PRR_kwDOSNjZ2s8AAAABM7VI_A at 2026-09-10T03:57:39Z. Current review-thread inventory is empty. This satisfies the independent current-head review lane; it does not substitute for executable product tests.

Exact 558c830... still has zero PR-triggered Actions workflow runs. Fresh protected Naruon workflows explain the absence: feature-base stacked PRs do not match the current Application CI/Bandit base filters even though protected guidance describes stacked verification. Canonical CI-owner defect .github#2073 now owns that mismatch. Do not retarget to develop, push a dummy commit, copy central workflow source, or promote status-only review signals as test evidence.

Keep Draft until exact-head executable checks exist through the canonical owner path and upstream #1623 is protected-integrated. No generated .jules note, duplicate dependency source, dummy requeue, self-approval, provider/model workaround, or gate weakening is carried here.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: c8f4065c-b4b3-4518-9695-a272a96731a1

📥 Commits

Reviewing files that changed from the base of the PR and between 17a7618 and 558c830.

📒 Files selected for processing (2)
  • backend/core/local_http.py
  • backend/tests/test_local_http.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The request target validator now iteratively decodes percent-encoded path segments, rejects invalid decoded escapes, and limits decoding depth. Tests cover safe literals, nested unsafe segments, excessive nesting, and invalid encoding.

Changes

Local request validation

Layer / File(s) Summary
Iterative path decoding
backend/core/local_http.py
The validator detects invalid decoded percent escapes and decodes each path segment up to 10 passes. It raises errors for invalid escapes and excessive encoding depth.
Decoding validation tests
backend/tests/test_local_http.py
Tests cover preserved %25 segments, nested encoded unsafe segments, excessive encoding depth, and additional invalid percent-encoding inputs.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 558c8

Local HTTP request-target validation now rejects nested unsafe or malformed percent encodings while preserving safe literal percent paths. The bounded decoding behavior and relevant edge cases are covered, with no remaining merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the local HTTP security fix and the bounded nested percent-decoding change. It is concise and related to the main changeset.
✨ 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 autoresearch/local-http-nested-decoding

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.

Copy link
Copy Markdown
Contributor Author

@codex review

Review exact head ff7f85a54f2027fc7d35748c916aa066e8072739 against exact base 17a7618eda2b212b691f08fa936e042b34258fc9. Verify only the two-file nested-decoding contract: source-order RED coverage, bounded ten-round convergence, traversal/backslash/control rejection, excessive-depth fail closed, preservation of safe path/query semantics, and the deliberately narrowed defense-in-depth severity. Please flag any off-by-one or malformed-percent case that escapes the current contract.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact ff7f85a54f2027fc7d35748c916aa066e8072739 only. Focus on the two changed files and the nested percent-decoding boundary, especially off-by-one depth, nested malformed percent escapes, safe literal-percent behavior, traversal/backslash/control-character rejection, and whether any finding would require changing the deliberately narrow defense-in-depth scope.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='17a7618eda2b212b691f08fa936e042b34258fc9'
head='ff7f85a54f2027fc7d35748c916aa066e8072739'

git cat-file -e "${base}^{commit}"
git cat-file -e "${head}^{commit}"
printf '%s\n' '=== ancestry ==='
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'
printf '%s\n' '=== changed files ==='
git diff --name-status "$base" "$head"
printf '%s\n' '=== exact diff ==='
git diff --find-renames --find-copies --unified=80 "$base" "$head" -- backend/core/local_http.py backend/tests/test_local_http.py
printf '%s\n' '=== source outline ==='
ast-grep outline backend/core/local_http.py --items all || true
printf '%s\n' '=== test outline ==='
ast-grep outline backend/tests/test_local_http.py --items all || true
printf '%s\n' '=== head source ==='
cat -n backend/core/local_http.py
printf '%s\n' '=== head test file ==='
cat -n backend/tests/test_local_http.py
printf '%s\n' '=== source-order RED commit ==='
git show --format=fuller --stat 763854fe229ca89e39497d00dea6e2b4848ee310
git show --format= --unified=50 763854fe229ca89e39497d00dea6e2b4848ee310 -- backend/core/local_http.py backend/tests/test_local_http.py

Length of output: 27417


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='ff7f85a54f2027fc7d35748c916aa066e8072739'

printf '%s\n' '=== commits from base to head ==='
git log --reverse --format='%H %s' '17a7618eda2b212b691f08fa936e042b34258fc9..'"$head"

printf '%s\n' '=== validate_local_request_target definitions and call sites at head ==='
git grep -n -C 6 'validate_local_request_target' "$head" -- ':!backend/tests/test_local_http.py'

printf '%s\n' '=== request construction near every call-site file ==='
git grep -l 'validate_local_request_target' "$head" -- ':!backend/tests/test_local_http.py' |
while IFS= read -r file; do
  printf '\n--- %s ---\n' "$file"
  git show "$head:$file" | nl -ba | sed -n '/validate_local_request_target/,+35p'
done

printf '%s\n' '=== percent-related tests at head ==='
git grep -n -i -C 3 'percent\|literal' "$head" -- backend/tests/test_local_http.py backend/core/local_http.py

printf '%s\n' '=== direct URL/request calls using validated target at head ==='
git grep -n -E -C 5 '(urljoin|requests\.|httpx\.|urllib\.request|client\.(get|post|request)|get\(|post\(|request\()' \
  "$head" -- backend | head -n 500

Length of output: 9549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='ff7f85a54f2027fc7d35748c916aa066e8072739'

printf '%s\n' '=== exact-head consumer files ==='
git grep -l 'validate_local_request_target' "$head" -- ':!backend/core/local_http.py' ':!backend/tests/test_local_http.py' |
sed -E 's|^[^:]+:||' |
while IFS= read -r file; do
  printf '\n--- %s ---\n' "$file"
  git show "$head:$file" | nl -ba | sed -n '/def _validated_/,/^[[:space:]]*$/p;/validate_local_request_target/,+100p'
done

printf '%s\n' '=== decoding calls in consumers ==='
git grep -n -E -C 4 '(unquote|unquote_plus|percent[-_ ]decode|decode\()' "$head" -- \
  backend/scripts/private_mail_http_smoke.py backend/tests/live/test_live_api_sequence.py || true

printf '%s\n' '=== exact-head local-http test cases for decoding boundary ==='
git show "$head:backend/tests/test_local_http.py" | nl -ba |
grep -n -E -C 5 'nested|excessive|percent|literal|preserves_safe' || true

printf '%s\n' '=== exact-head line-numbered changed helper ==='
git show "$head:backend/core/local_http.py" | nl -ba | sed -n '90,145p'

Length of output: 71249


@seonghobae Found one defense-in-depth contract gap.

P3 — Reject malformed percent escapes after nested decoding

backend/core/local_http.py, Line 108 validates percent escapes only in the original parsed.path. A valid outer escape can produce a malformed escape during a later decode round. For example, "/api/%2525GG" decodes as follows:

%2525GG -> %25GG -> %GG

unquote("%GG", errors="strict") leaves %GG unchanged. The loop then treats the segment as stable and accepts it. This bypasses the invalid-percent rule for nested input.

Validate _INVALID_PERCENT_ESCAPE after each changed decode result, or at least once on the final stable decoded_segment. Add regression cases for nested malformed escapes such as "/api/%2525GG" and "/api/%252".

The ten-round boundary is correct. A segment that becomes stable after exactly ten decode changes is accepted. A segment that needs an eleventh change is rejected. Valid %25 input also remains accepted as a literal percent character.

The static consumers pass the original validated target directly to HTTPConnection.request or HTTPSConnection.request. I found no explicit downstream decode. This finding does not establish production SSRF or require a broader severity claim. It remains a narrow defense-in-depth validation issue.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please re-review current exact 558c830651b64c2434cb64791a804f8059da6773 only. The prior P3 nested-malformed-percent finding is addressed through RED 709cb1b4... then fix 558c830.... Verify /api/%2525GG and /api/%252 now fail closed, /api/literal%25 remains accepted unchanged, the ten-round boundary is unchanged, and no broader production SSRF claim is warranted.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='17a7618eda2b212b691f08fa936e042b34258fc9'
red1='763854fe229ca89e39497d00dea6e2b4848ee310'
fix1='ff7f85a54f2027fc7d35748c916aa066e8072739'
red2='709cb1b4bf05fa549be66c0e448e32902dbeffcc'
head='558c830651b64c2434cb64791a804f8059da6773'

for commit in "$base" "$red1" "$fix1" "$red2" "$head"; do
  git cat-file -e "${commit}^{commit}"
done

printf '%s\n' '=== ancestry and commits ==='
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'
git log --reverse --format='%H %s' "${base}..${head}"

printf '%s\n' '=== exact effective delta ==='
git diff --name-status "$base" "$head"
git diff --unified=100 "$base" "$head" -- backend/core/local_http.py backend/tests/test_local_http.py

printf '%s\n' '=== commit progression ==='
for commit in "$red1" "$fix1" "$red2" "$head"; do
  printf '\n--- %s ---\n' "$commit"
  git show --format='%H%n%s' --stat "$commit"
  git show --format= --unified=60 "$commit" -- backend/core/local_http.py backend/tests/test_local_http.py
done

printf '%s\n' '=== exact-head helper structure ==='
ast-grep outline backend/core/local_http.py --items all
printf '%s\n' '=== exact-head helper and decode-related tests ==='
nl -ba backend/core/local_http.py | sed -n '1,220p'
nl -ba backend/tests/test_local_http.py | grep -E -C 12 'validate_local_request_target|percent|nested|literal|excessive|round|traversal|backslash|control'

printf '%s\n' '=== exact-head consumers and decode calls ==='
git grep -n -C 10 'validate_local_request_target' "$head" -- ':!backend/core/local_http.py' ':!backend/tests/test_local_http.py' || true
git grep -n -E -C 4 '\b(unquote|unquote_plus|decode)\s*\(' "$head" -- backend || true

Length of output: 131022


@seonghobae Re-review complete for exact commit 558c830651b64c2434cb64791a804f8059da6773.

I found no unresolved findings in the requested scope.

  • backend/core/local_http.py, Line 118 checks every decoded layer for malformed nonterminal percent escapes.
  • /api/%2525GG decodes to %GG and fails closed.
  • /api/%252 decodes to %2 and fails closed.
  • /api/literal%25 decodes to a terminal literal %. The helper accepts it and returns the original target unchanged.
  • The ten-round boundary is unchanged. Ten decode changes can converge and pass. An eleventh required change fails closed.
  • The retained RED coverage includes both malformed nested cases and the literal-percent case.
  • Static consumer inspection shows local smoke and live-test request use. I found no downstream percent decoding of the validated target.

This remains a narrow defense-in-depth validation change. The reviewed code does not support a broader production SSRF claim.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Re-review exact head 558c830651b64c2434cb64791a804f8059da6773 against exact base 17a7618eda2b212b691f08fa936e042b34258fc9 now that the prior transient review-capacity window has elapsed. Scope only backend/core/local_http.py and backend/tests/test_local_http.py. Verify every recursive decode layer rejects malformed percent escapes, safe literal %25 remains accepted, traversal/backslash/control rejection is preserved, ten decode rounds converge safely, an eleventh active decode fails closed, and the code does not overclaim a production SSRF chain.

@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='17a7618eda2b212b691f08fa936e042b34258fc9'
head='558c830651b64c2434cb64791a804f8059da6773'
red1='763854fe229ca89e39497d00dea6e2b4848ee310'
red2='709cb1b4bf05fa549be66c0e448e32902dbeffcc'

for commit in "$base" "$head" "$red1" "$red2"; do
  git cat-file -e "${commit}^{commit}"
done

printf '%s\n' '=== ancestry and effective delta ==='
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'
git diff --name-status "$base" "$head"
git diff --unified=100 "$base" "$head" -- backend/core/local_http.py backend/tests/test_local_http.py

printf '%s\n' '=== exact-head structure ==='
ast-grep outline backend/core/local_http.py --items all
ast-grep outline backend/tests/test_local_http.py --items all

printf '%s\n' '=== exact-head validation helper ==='
git show "$head:backend/core/local_http.py" | nl -ba | sed -n '1,240p'

printf '%s\n' '=== exact-head relevant tests ==='
git show "$head:backend/tests/test_local_http.py" | nl -ba | \
  grep -E -C 14 'validate_local_request_target|percent|nested|literal|excessive|round|traversal|backslash|control'

printf '%s\n' '=== source-order RED patches ==='
for commit in "$red1" "$red2"; do
  printf '\n--- %s ---\n' "$commit"
  git show --format='%H%n%s' --unified=80 "$commit" -- \
    backend/core/local_http.py backend/tests/test_local_http.py
done

printf '%s\n' '=== exact-head consumers and decode operations ==='
git grep -n -C 12 'validate_local_request_target' "$head" -- \
  ':!backend/core/local_http.py' ':!backend/tests/test_local_http.py' || true
git grep -n -E -C 4 '\b(unquote|unquote_plus|decode)\s*\(' "$head" -- backend || true

Length of output: 123704


🏁 Script executed:

#!/bin/bash
set -euo pipefail

head='558c830651b64c2434cb64791a804f8059da6773'

for file in \
  backend/scripts/private_mail_http_smoke.py \
  backend/tests/live/test_live_api_sequence.py
do
  printf '\n=== %s: structure ===\n' "$file"
  ast-grep outline "$file" --items all
  printf '\n=== %s: validated target and HTTP request contexts ===\n' "$file"
  git show "$head:$file" | nl -ba | \
    grep -E -C 18 'validate_local_request_target|HTTP(S)?Connection|\.request\s*\('
done

Length of output: 13894


@seonghobae Re-review complete for exact commit 558c830651b64c2434cb64791a804f8059da6773 against exact base 17a7618eda2b212b691f08fa936e042b34258fc9.

I found no unresolved findings in backend/core/local_http.py or backend/tests/test_local_http.py.

  • The effective delta changes only the requested two files.
  • Line 118 validates each decoded layer for malformed percent escapes.
  • /api/%2525GG reaches %GG and fails closed.
  • /api/%252 reaches %2 and fails closed.
  • /api/literal%25 reaches a terminal literal %. The helper accepts it and returns the original target unchanged.
  • Nested traversal, backslash, and control-character values still fail closed.
  • Ten decode changes can converge safely. If an eleventh decode would change the segment, Line 126 rejects the request.
  • The source-order RED commits retain coverage for nested unsafe segments, excessive depth, nested malformed escapes, and the literal-percent case.
  • The inspected consumers pass the validated target directly to HTTPConnection.request or HTTPSConnection.request. They do not apply downstream percent decoding.

This is a narrow defense-in-depth validation change. The reviewed code does not establish a production SSRF chain.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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