Skip to content

fix(route-rules): reject out-of-scope requests#4222

Merged
pi0 merged 4 commits intomainfrom
fix/proxy-rule-normalize
Apr 22, 2026
Merged

fix(route-rules): reject out-of-scope requests#4222
pi0 merged 4 commits intomainfrom
fix/proxy-rule-normalize

Conversation

@pi0
Copy link
Copy Markdown
Member

@pi0 pi0 commented Apr 22, 2026

A proxy route rule like /api/orders/**: { proxy: { to: "http://upstream/orders/**" } } could be bypassed by requesting ..%2f the encoded slash stayed opaque at match time, so Nitro matched the /** scope and forwarded the raw path to the upstream, which decoded %2f → / and escaped the intended scope.

Note

This depends on how the upstream interprets %2F. Frameworks that honor RFC 3986 and treat %2F as opaque within path segments are already safe (including Express, H3, and Hono).

The fix also assumes the upstream does not double-decode percent-encoding: %252F stays opaque after canonicalization, so an upstream that decodes twice (%252F → %2F → /) could still be coaxed into escaping the scope. Single-decode is standard behavior.

Fix

Before building the upstream URL in the proxy route rule, canonicalize the incoming pathname and verify it still falls under the rule's base path. If not, respond with 400 Bad Request instead of proxying. The bytes forwarded upstream are unchanged when the request is allowed — only rejection behavior is new.

Canonicalization uses new URL: %2F and %5C are pre-decoded (WHATWG URL keeps them opaque in paths), then URL parsing resolves ./../%2E%2E segments and normalizes \.


e2e reproduction:

backend.mjs

import { createServer } from "node:http";

const routes = {
  "/orders/list.json": { orders: [{ id: 1, item: "widget" }] },
  "/admin/config.json": {
    secret: "STRIPE_SK_LIVE_xyz123",
    db_password: "pr0duct10n_p4ss!",
  },
  "/admin/users.json": {
    users: [
      {
        id: 1,
        email: "admin@company.com",
        password_hash: "5f4dcc3b5aa765d61d8327deb882cf99",
      },
    ],
  },
};

createServer((req, res) => {
  const decoded = decodeURIComponent((req.url ?? "/").split("?")[0]);
  const normalized = new URL(decoded, "http://x").pathname;
  const body = routes[normalized];
  res.writeHead(body ? 200 : 404, {
    "content-type": body ? "application/json" : "text/plain",
  });
  res.end(body ? JSON.stringify(body) : `404 ${normalized}`);
}).listen(4444, () => {
  console.log("fake upstream listening on http://localhost:4444");
});

nitro.config.ts

import { defineConfig } from "nitro";

export default defineConfig({
  routeRules: {
    "/api/orders/**": { proxy: { to: "http://localhost:4444/orders/**" } },
  },
});

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nitro.build Ready Ready Preview, Comment Apr 22, 2026 8:52am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ada9003-6233-430c-8c7e-541956b5de8b

📥 Commits

Reviewing files that changed from the base of the PR and between e3e654c and 5ee65d8.

📒 Files selected for processing (2)
  • src/runtime/internal/route-rules.ts
  • test/unit/route-rules.test.ts

📝 Walkthrough

Walkthrough

Adds path scope validation to prevent directory traversal attacks in route rules. A new isPathInScope() helper canonicalizes URL pathnames and validates they remain within a specified base. The redirect and proxy route rules now validate paths against their respective base values and reject out-of-scope requests with HTTP 400 errors.

Changes

Cohort / File(s) Summary
Route Rules Implementation
src/runtime/internal/route-rules.ts
Imports HTTPError from h3 and adds exported isPathInScope(pathname, base) helper that canonicalizes pathnames (pre-decoding %2f/%5c) via new URL() and validates they remain within scope. Updates redirect and proxy route rules to validate event.url.pathname against their respective base values when _redirectStripBase or _proxyStripBase is present, throwing HTTPError({ status: 400 }) if out of scope.
Route Rules Tests
test/unit/route-rules.test.ts
Adds comprehensive test suite for isPathInScope() covering path-scoping behavior including paths within scope, encoded traversal attempts (%2f, %2F, %5c), double-encoded segments (%2E%2E), literal traversal escapes (../, ../../), prefix mismatches, and catch-all behavior for empty bases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows conventional commits format with 'fix' prefix, scope 'route-rules', and a clear description of the change.
Description check ✅ Passed The PR description comprehensively explains the security issue, the fix mechanism, assumptions about upstream behavior, and includes a detailed e2e reproduction example.
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/proxy-rule-normalize

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.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 22, 2026

Open in StackBlitz

npm i https://pkg.pr.new/nitro@4222

commit: 56b6f84

@pi0 pi0 changed the title fix(route-rukles): reject out-of-scope proxy requests fix(route-rules): reject out-of-scope proxy requests Apr 22, 2026
@pi0 pi0 changed the title fix(route-rules): reject out-of-scope proxy requests fix(route-rules): reject out-of-scope requests Apr 22, 2026
pi0 added 2 commits April 22, 2026 10:49
Mirrors the scope check added for proxy rules. An encoded traversal like
`..%2f` bypasses the `/**` scope at match time but can escape the base
once the redirect target decodes `%2f` → `/`, letting a victim's
browser reach a sibling scope on the redirect host.
@pi0 pi0 marked this pull request as ready for review April 22, 2026 08:52
@pi0 pi0 merged commit 135f762 into main Apr 22, 2026
10 checks passed
@pi0 pi0 deleted the fix/proxy-rule-normalize branch April 22, 2026 12:56
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