Skip to content

tool_ipfs: keep the path percent-encoded when rewriting to the gateway - #22944

Closed
1rhino2 wants to merge 1 commit into
curl:masterfrom
1rhino2:ipfs-reject-encoded-dotdot
Closed

1rhino2 wants to merge 1 commit into
curl:masterfrom
1rhino2:ipfs-reject-encoded-dotdot

Conversation

@1rhino2

@1rhino2 1rhino2 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

The IPFS/IPNS rewrite pulls the user path (and the CID and the gateway path)
with CURLU_URLDECODE, drops it after the gateway base and the /ipfs/<cid>
(or /ipns/<name>) part, and re-encodes the result with CURLU_URLENCODE.
The decode happens after the URL parser already did its own dot-segment
cleanup, so an encoded slash (%2f) turns into a segment separator too late to
be cleaned, and a .. then climbs back out of the namespace when the final URL
is parsed.

So e.g. ipfs://<cid>/%2f..%2f..%2f..%2f..%2f..%2fadmin/config with a gateway
of http://host/private/gateway ends up requesting /admin/config on that
origin instead of staying under /private/gateway/ipfs/<cid>/.

Fix (bagder's suggestion below): do not decode at all. Get the CID, gateway
path and user path still encoded and set the combined path without
re-encoding, so a %2f stays %2f and never becomes a separator. That also
covers the double-encoded %252e%252f variant, and a plain ../ is still
collapsed by the parser when the ipfs:// URL is first parsed, so it resolves
inside the namespace like before.

Tests 1688 (ipfs) and 1689 (ipns) send the encoded dotdot path through a
gateway with a base path and check the request stays under
/private/gateway/ipfs|ipns/<cid>/. Both fail on master with a request for
/admin/config. The existing IPFS tests 722-741 still pass, checksrc is clean.

Reported via HackerOne (#4027511).

Copilot AI 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.

🟡 Changes recommended

Unresolved double-encoded traversal risk and missing regression coverage must be addressed.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Hardens IPFS/IPNS gateway rewriting against decoded .. path traversal.

Changes:

  • Validates decoded path segments.
  • Rejects traversal attempts with CURLE_URL_MALFORMAT.
File summaries
File Summary
src/tool_ipfs.c Adds dot-segment validation during IPFS/IPNS gateway rewriting.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/tool_ipfs.c Outdated

/* reject a decoded ".." segment so the request cannot escape the gateway's
content namespace (result stays CURLE_URL_MALFORMAT) */
if(inputpath && has_dotdot_segment(inputpath))
Comment thread src/tool_ipfs.c Outdated

/* reject a decoded ".." segment so the request cannot escape the gateway's
content namespace (result stays CURLE_URL_MALFORMAT) */
if(inputpath && has_dotdot_segment(inputpath))
@bagder

bagder commented Sep 14, 2026

Copy link
Copy Markdown
Member

it struck me that curl should probably just not URL decode the path (nor any other parts) when extracted from the URL, so that it builds the new URL with already URL encoded parts.

@1rhino2
1rhino2 force-pushed the ipfs-reject-encoded-dotdot branch from 7061b86 to 3d5752c Compare September 14, 2026 20:19
@1rhino2

1rhino2 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

went with your suggestion. it doesn't decode the path parts anymore, just builds the new url from the still-encoded cid, gateway path and user path and sets the path without re-encoding, so a %2f stays %2f and never becomes a separator. that also kills the double-encoded %252e%252f variant copilot pointed at, and a plain ../ still gets collapsed by the parser at parse time so it resolves inside the namespace like before.

i left scheme/host/port alone since they come from the gateway and get set as their own url parts, no attacker input rides through them, but can make it uniform if you'd rather. checked single and double encoded ipfs+ipns stay under the /ipfs|ipns// prefix, normal paths and a literal admin/config are unchanged, 20 ipfs tests still pass. no test in the PR yet, couldn't find a free number near the 72x ones, tell me which to use and i'll add one that checks the encoded path survives.

@1rhino2
1rhino2 force-pushed the ipfs-reject-encoded-dotdot branch from 3d5752c to 834af0f Compare September 14, 2026 22:45
@1rhino2 1rhino2 changed the title tool_ipfs: reject ".." segments in the decoded IPFS path tool_ipfs: keep the path percent-encoded when rewriting to the gateway Sep 14, 2026
@1rhino2

1rhino2 commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

added tests 1688 and 1689 (ipfs and ipns) since those slots were free, they both fail on master with the request going to /admin/config and pass with the change. also updated the title and description, the old ones still described the reject-dotdot version.

@github-actions github-actions Bot added the tests label Sep 14, 2026
@bagder
bagder requested a lite review from Copilot September 17, 2026 20:29
@bagder

bagder commented Sep 17, 2026

Copy link
Copy Markdown
Member

Ah, it looks as if something else stole those test numbers in the mean time. Can you move 1688 and 1689 to other numbers? ./scripts/testsnum helps you find them.

Copilot AI 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.

🟢 Approval recommended

The focused rewrite change addresses the traversal case and includes matching IPFS and IPNS regression coverage.

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

The IPFS/IPNS rewrite fetched the user path (and the CID and gateway path)
with CURLU_URLDECODE and then re-encoded the assembled path with
CURLU_URLENCODE. The decode turned an encoded slash (%2f) into a real
separator that the URL parser's earlier dot-segment cleanup never saw, so a
".." could climb back out of the /<protocol>/<cid> namespace when the final
URL was parsed. A double-encoded form (%252e%252f) slipped through the same
way.

Build the new URL from the still-encoded parts instead: get the CID, the
gateway path and the user path without decoding, and set the combined path
without re-encoding. A percent-encoded slash then stays encoded, so no new
separators appear and the later normalization has nothing extra to remove. A
literal "../" in the user path is still collapsed by the parser when the
ipfs:// URL is first parsed, so it resolves within the namespace as before.

Add tests 1691 (ipfs) and 1692 (ipns) that send an encoded-slash dotdot path
through a gateway with a base path and verify the request stays under
/<gateway>/ipfs|ipns/<cid>/. Both fail on the previous code with a request
for /admin/config.

Reported-by: 1rhino2 on hackerone
@1rhino2
1rhino2 force-pushed the ipfs-reject-encoded-dotdot branch from 834af0f to 1b10ed2 Compare September 17, 2026 22:55
@1rhino2

1rhino2 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

moved them to 1691 and 1692, next free ones after the block. rebased on master too so the conflict is gone. both still fail on the old code (request goes to /admin/config) and pass with the fix, and 1688/1689 from master are untouched.

@bagder bagder closed this in ae08995 Sep 18, 2026
@bagder

bagder commented Sep 18, 2026

Copy link
Copy Markdown
Member

Thanks!

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

Development

Successfully merging this pull request may close these issues.

3 participants