Skip to content

fix(docs): prevent path traversal in /docs static handler#80

Open
pescn wants to merge 1 commit intomainfrom
codex/propose-fix-for-path-traversal-vulnerability-cutd7x
Open

fix(docs): prevent path traversal in /docs static handler#80
pescn wants to merge 1 commit intomainfrom
codex/propose-fix-for-path-traversal-vulnerability-cutd7x

Conversation

@pescn
Copy link
Contributor

@pescn pescn commented Mar 22, 2026

Motivation

  • The /docs/* handler constructed filesystem paths from request paths without normalization or traversal checks, allowing unauthenticated arbitrary file reads outside the docs directory.
  • The __tsr asset route used the same unsafe pattern and remained exposed.

Description

  • Add a small containment helper resolvePathWithinBase in backend/src/utils/safePath.ts that normalizes request paths, rejects .. segments, and resolves into the configured base directory.
  • Use resolvePathWithinBase in backend/src/index.ts for the /docs/* handler and the /__tsr/* asset route to return 404 when a path would escape the docs directory instead of reading arbitrary files.
  • Add focused regression tests in backend/src/utils/safePath.test.ts covering in-base resolution, root requests, traversal rejection, and repeated leading slashes.

Testing

  • Ran the focused unit test with bun test ./src/utils/safePath.test.ts, which passed all tests.
  • Ran git diff --check to validate the diff, which reported no whitespace errors.
  • Attempted bun run check for broader type checks, which failed due to missing repository dependencies/type definitions in the environment (pre-existing module resolution/type errors unrelated to this patch).

Codex Task

Summary by CodeRabbit

发布说明

  • 新功能

    • 改进了静态文件提供机制,增强了文件路径处理的可靠性。
  • 测试

    • 添加了文件路径解析的测试覆盖。

@coderabbitai
Copy link

coderabbitai bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

该更改引入了安全路径解析功能,通过新增 resolvePathWithinBase() 函数防止目录遍历攻击,并在静态文件服务处理中集成该函数来验证请求路径的合法性。

Changes

Cohort / File(s) Summary
安全路径解析实现
backend/src/utils/safePath.ts
添加 resolvePathWithinBase(baseDir, requestPath) 函数,用于规范化请求路径并防止目录遍历,拒绝包含 ".." 的路径片段。
安全路径解析测试
backend/src/utils/safePath.test.ts
新增 Bun 测试套件验证函数行为:嵌套路径解析、根路径映射、目录遍历拒绝、重复前导斜杠处理。
静态文件服务集成
backend/src/index.ts
更新文档和 TanStack Start 路由的静态文件处理逻辑,使用 resolvePathWithinBase() 验证所有请求路径,无效路径返回 404。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰✨ 安全的小径在我前方延伸,
不怕 .. 的诡计困扰,
每个请求都被温柔地检验,
目录遍历已成过往烟云,
代码稳若磐石,守护数据如珍宝! 🛡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 拉取请求标题清晰地总结了主要更改内容,即修复/docs静态处理程序中的路径遍历漏洞。

✏️ 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 codex/propose-fix-for-path-traversal-vulnerability-cutd7x

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.

Tip

CodeRabbit can use Trivy to scan for security misconfigurations and secrets in Infrastructure as Code files.

Add a .trivyignore file to your project to customize which findings Trivy reports.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a potential security vulnerability by preventing path traversal in the /docs/* handler and the __tsr asset route. It introduces a utility function to safely resolve paths within a specified base directory, ensuring that requests cannot access files outside of the intended documentation directory. Regression tests are added to validate the fix.

Highlights

  • Path Traversal Prevention: This PR introduces a security enhancement to prevent path traversal vulnerabilities in the /docs/* handler and the __tsr asset route.
  • Safe Path Resolution: A new utility function, resolvePathWithinBase, is added to normalize request paths, reject .. segments, and resolve paths within a configured base directory.
  • Regression Tests: Focused regression tests are included to verify the correct behavior of resolvePathWithinBase, covering in-base resolution, root requests, traversal rejection, and repeated leading slashes.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a security fix to prevent path traversal in the /docs and /__tsr static file handlers. The approach of creating a resolvePathWithinBase utility is good. My review includes a suggestion to make this utility more robust by allowing valid relative paths while still preventing traversal.

Critically, a similar path traversal vulnerability appears to have been missed in the spaPlugin which handles the /* route. The static file serving logic in spaPlugin (around line 181 in backend/src/index.ts) uses path.join without sanitization and should also be updated to use resolvePathWithinBase to prevent arbitrary file reads.

I have also added a suggestion for an additional test case to cover the improved path resolution logic.

Comment on lines +1 to +18
import { resolve } from "node:path";

export function resolvePathWithinBase(
baseDir: string,
requestPath: string,
): string | null {
const normalizedRequestPath = requestPath.replaceAll("\\", "/");
const pathSegments = normalizedRequestPath
.replace(/^\/+/, "")
.split("/")
.filter(Boolean);

if (pathSegments.includes("..")) {
return null;
}

return resolve(baseDir, ...pathSegments);
}

Choose a reason for hiding this comment

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

security-medium medium

The current implementation is a bit too restrictive as it rejects any path containing .., even if it resolves to a path still within the base directory (e.g., /foo/../bar). A more robust approach is to resolve the path first and then verify that it is still within the intended base directory. This allows for valid relative paths while still preventing directory traversal attacks. I've also added a check for null byte injection as an extra precaution.

import { isAbsolute, relative, resolve } from "node:path";

export function resolvePathWithinBase(
  baseDir: string,
  requestPath: string,
): string | null {
  // Prevent null byte injection.
  if (requestPath.includes("\u0000")) {
    return null;
  }

  // Resolve the path. This will process '..', '.', etc.
  // We must treat requestPath as relative to baseDir, so we remove leading slashes.
  const resolvedPath = resolve(baseDir, requestPath.replace(/^\/+/, ""));

  // Check if the resolved path is a sub-path of baseDir.
  const relation = relative(baseDir, resolvedPath);

  // If `relation` is empty, it's the base directory itself.
  // If it starts with '..', it has escaped the base directory.
  // An absolute path as `relation` is also an escape.
  if (relation.startsWith("..") || isAbsolute(relation)) {
    return null;
  }

  return resolvedPath;
}

expect(resolvePathWithinBase(baseDir, "//etc/passwd")).toBe(
resolve(baseDir, "etc/passwd"),
);
});

Choose a reason for hiding this comment

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

medium

To complement the suggested change in safePath.ts, it would be beneficial to add a test case that verifies that valid paths containing .. which resolve within the base directory are handled correctly.

    );
  });

  test("resolves valid paths with '..' segments that stay within the base directory", () => {
    expect(
      resolvePathWithinBase(baseDir, "/assets/css/../js/app.js"),
    ).toBe(resolve(baseDir, "assets/js/app.js"));
  });

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/index.ts (1)

180-184: ⚠️ Potential issue | 🔴 Critical

严重安全漏洞:spaPlugin 仍存在路径遍历漏洞。

docsPlugin 已修复,但 spaPlugin 的第 181 行仍直接使用 join(dir, path) 构建文件路径,未应用任何遍历检查。攻击者可通过类似 GET /../../etc/passwd 的请求读取任意文件。

🐛 修复 spaPlugin 中的路径遍历漏洞
     .get("/*", async ({ path, status }) => {
       // Skip /docs and /__tsr routes - they're handled by docsPlugin
       if (path.startsWith("/docs") || path.startsWith("/__tsr")) {
         return status(404);
       }
       // Skip API routes and metrics (include trailing slash to prevent SPA fallback)
       if (
         path.startsWith("/api") ||
         path.startsWith("/v1") ||
         path === "/metrics" ||
         path === "/metrics/"
       ) {
         return status(404);
       }

       // Try to serve as static file first
-      const staticResponse = await serveStaticFile(join(dir, path));
+      const safePath = resolvePathWithinBase(dir, path);
+      const staticResponse = safePath ? await serveStaticFile(safePath) : null;
       if (staticResponse) {
         return staticResponse;
       }

       // Fall back to index.html for SPA routing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/index.ts` around lines 180 - 184, spaPlugin still constructs
filesystem paths directly using join(dir, path) before calling serveStaticFile,
leaving a path traversal vulnerability; update spaPlugin to validate/sanitize
the requested path (normalize and ensure it stays within the base dir) before
joining or serving: resolve and normalize the incoming path, reject or
canonicalize any paths that escape the base directory (e.g., containing ../
after normalization), then call serveStaticFile with the safe path (references:
spaPlugin, serveStaticFile, join(dir, path)); if the normalized path is outside
dir return a 403/404 instead of serving the file.
🧹 Nitpick comments (2)
backend/src/utils/safePath.test.ts (1)

18-26: 建议增加 URL 编码和其他边界情况的测试用例。

当前测试覆盖了基本场景,但缺少一些安全关键的边界情况测试,建议补充:

🧪 建议添加的测试用例
   test("rejects traversal outside the base directory", () => {
     expect(resolvePathWithinBase(baseDir, "/../../etc/passwd")).toBeNull();
   });

+  test("handles URL-encoded path segments as literals", () => {
+    // %2e%2e should be treated as literal directory name, not traversal
+    expect(resolvePathWithinBase(baseDir, "/%2e%2e/etc/passwd")).toBe(
+      resolve(baseDir, "%2e%2e/etc/passwd"),
+    );
+  });
+
+  test("rejects mixed traversal attempts", () => {
+    expect(resolvePathWithinBase(baseDir, "/foo/../../../etc/passwd")).toBeNull();
+  });
+
+  test("handles empty path", () => {
+    expect(resolvePathWithinBase(baseDir, "")).toBe(baseDir);
+  });
+
   test("contains repeated leading slashes within the base directory", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/safePath.test.ts` around lines 18 - 26, Add tests in
safePath.test.ts for resolvePathWithinBase to cover URL-encoded and other edge
cases: verify encoded traversal sequences (e.g., "%2e%2e" and "%2f"), mixed
separators and redundant segments (e.g., "subdir/../etc", "subdir\\..\\etc"),
inputs with trailing dots or dot-segments ("./file", "../file"), empty or
multiple consecutive slashes, and percent-encoded slashes; for each case assert
that traversal outside base returns null and valid in-base paths resolve to
resolve(baseDir, "...") using the existing resolvePathWithinBase helper to
ensure the function properly decodes/normalizes before validation.
backend/src/utils/safePath.ts (1)

13-17: 建议增加纵深防御:验证解析后的路径确实位于基目录内。

当前实现通过检查字面量 .. 来防止目录遍历。虽然 URL 编码的遍历序列(如 %2e%2e)不会构成真正的威胁——因为浏览器在发送请求前已解码路径,而 Elysia 不会再次解码——但从安全纵深防御角度,建议在解析后额外验证最终路径确实位于基目录内。

🛡️ 建议的纵深防御修复
 import { resolve } from "node:path";

 export function resolvePathWithinBase(
   baseDir: string,
   requestPath: string,
 ): string | null {
   const normalizedRequestPath = requestPath.replaceAll("\\", "/");
   const pathSegments = normalizedRequestPath
     .replace(/^\/+/, "")
     .split("/")
     .filter(Boolean);

   if (pathSegments.includes("..")) {
     return null;
   }

-  return resolve(baseDir, ...pathSegments);
+  const resolvedBase = resolve(baseDir);
+  const resolvedPath = resolve(baseDir, ...pathSegments);
+
+  // Defense-in-depth: ensure resolved path is within base directory
+  if (!resolvedPath.startsWith(resolvedBase + "/") && resolvedPath !== resolvedBase) {
+    return null;
+  }
+
+  return resolvedPath;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/utils/safePath.ts` around lines 13 - 17, The current check only
rejects literal ".." segments; after computing resolvedPath with
resolve(baseDir, ...pathSegments) (using resolve and baseDir/pathSegments),
additionally verify the resolvedPath is inside baseDir: compute the absolute
resolved path and the absolute baseDir (using resolve) and reject (return null)
if the resolved path is outside the base (e.g., path.relative(baseDirAbs,
resolvedPath) starts with ".." or resolvedPath does not have baseDirAbs as its
prefix). Apply this check in safePath.ts right after calling resolve and before
returning the path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@backend/src/index.ts`:
- Around line 180-184: spaPlugin still constructs filesystem paths directly
using join(dir, path) before calling serveStaticFile, leaving a path traversal
vulnerability; update spaPlugin to validate/sanitize the requested path
(normalize and ensure it stays within the base dir) before joining or serving:
resolve and normalize the incoming path, reject or canonicalize any paths that
escape the base directory (e.g., containing ../ after normalization), then call
serveStaticFile with the safe path (references: spaPlugin, serveStaticFile,
join(dir, path)); if the normalized path is outside dir return a 403/404 instead
of serving the file.

---

Nitpick comments:
In `@backend/src/utils/safePath.test.ts`:
- Around line 18-26: Add tests in safePath.test.ts for resolvePathWithinBase to
cover URL-encoded and other edge cases: verify encoded traversal sequences
(e.g., "%2e%2e" and "%2f"), mixed separators and redundant segments (e.g.,
"subdir/../etc", "subdir\\..\\etc"), inputs with trailing dots or dot-segments
("./file", "../file"), empty or multiple consecutive slashes, and
percent-encoded slashes; for each case assert that traversal outside base
returns null and valid in-base paths resolve to resolve(baseDir, "...") using
the existing resolvePathWithinBase helper to ensure the function properly
decodes/normalizes before validation.

In `@backend/src/utils/safePath.ts`:
- Around line 13-17: The current check only rejects literal ".." segments; after
computing resolvedPath with resolve(baseDir, ...pathSegments) (using resolve and
baseDir/pathSegments), additionally verify the resolvedPath is inside baseDir:
compute the absolute resolved path and the absolute baseDir (using resolve) and
reject (return null) if the resolved path is outside the base (e.g.,
path.relative(baseDirAbs, resolvedPath) starts with ".." or resolvedPath does
not have baseDirAbs as its prefix). Apply this check in safePath.ts right after
calling resolve and before returning the path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 52af95d6-32c2-44a8-963b-f5cf2c66bb24

📥 Commits

Reviewing files that changed from the base of the PR and between a394871 and d12c8e4.

📒 Files selected for processing (3)
  • backend/src/index.ts
  • backend/src/utils/safePath.test.ts
  • backend/src/utils/safePath.ts

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant