Skip to content

fix(template): reject path traversal in public template-by-id endpoint#3242

Open
baktun14 wants to merge 1 commit into
mainfrom
fix/api-templates-path-traversal
Open

fix(template): reject path traversal in public template-by-id endpoint#3242
baktun14 wants to merge 1 commit into
mainfrom
fix/api-templates-path-traversal

Conversation

@baktun14
Copy link
Copy Markdown
Contributor

@baktun14 baktun14 commented May 29, 2026

Why

GET /v1/templates/{id} (public, unauthenticated) built a file path by interpolating the caller-supplied id straight into the template cache path. Hono routes with decodeURI (so a URL-encoded %2F survives as a single segment), then decodes the param with decodeURIComponent, so a payload like ..%2F..%2Fsecret arrived at the service as ../../secret and escaped the cache directory — disclosing arbitrary .json files the API process can read (other tenants' cached templates, service-account key JSON, config JSON).

Found via an internal repo-wide security audit.

Fixes CON-428

What

  • New reusable resolvePathWithinDir() util (apps/api/src/utils/path.ts) that resolves a caller-supplied segment against a base dir and returns null unless it stays strictly inside it (rejects traversal, absolute escapes, and null bytes).
  • getTemplateById() now resolves the read path through the util. A rejected id logs a TEMPLATE_PATH_TRAVERSAL_BLOCKED warning and returns null, which the controller maps to a 404 (no info leak). The trusted cache-write path is unchanged.
  • Chose a containment check over a charset allowlist: legit ids are {owner}-{repo}-{path} and can contain dots (e.g. …-DeepSeek-V3.1) and other folder-name characters, so an allowlist would risk breaking valid lookups.
  • The legacy redirect route (/v1/templates/:id{…\.json}) only redirects and is transitively covered by the service guard — no change needed.

Tests

  • Util unit tests (path.spec.ts).
  • Service unit tests: traversal ids return null without reading any file; a legit dot-id still resolves to the correct path.
  • Functional test (test/functional/templates.spec.ts): encoded traversal payloads (..%2F..%2F..%2Fetc%2Fpasswd, %2Fetc%2Fpasswd, ..%2F..%2Fsecret, and double-encoded) all return 404 at the real endpoint.

No breaking changes; no DB migrations.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened security validation for template retrieval to prevent path traversal attacks.
    • API now safely rejects invalid template identifiers without exposing system files.

Review Change Stack

The public, unauthenticated GET /v1/templates/{id} endpoint built a file
path by interpolating the caller-supplied id straight into the template
cache path. Hono routes with decodeURI (so a URL-encoded %2F survives as a
single segment) and decodes the param with decodeURIComponent, so a payload
like ..%2F..%2Fsecret arrived at the service as ../../secret and escaped the
cache directory, disclosing arbitrary .json files the API can read.

Add a reusable resolvePathWithinDir() util that resolves a caller-supplied
segment against a base dir and returns null unless it stays strictly inside
it (rejecting traversal, absolute escapes, and null bytes). Guard the read in
getTemplateById() with it: a rejected id logs a TEMPLATE_PATH_TRAVERSAL_BLOCKED
warning and returns null, which the controller maps to a 404 (no info leak).
Legit ids — including ones with dots like ...-DeepSeek-V3.1 — are unaffected.

Covered by util unit tests, service unit tests (traversal returns null without
reading), and a functional test asserting encoded traversal payloads 404 at the
real endpoint.

Fixes CON-428
@baktun14 baktun14 requested a review from a team as a code owner May 29, 2026 21:08
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR adds path traversal protection to the template gallery service. A new utility resolvePathWithinDir safely validates that resolved paths remain within a base directory and rejects traversal attempts, null bytes, and absolute paths. The template service integrates this utility into getTemplateById, returning null and logging warnings for invalid paths. Unit and functional tests validate the protection across service and HTTP layers.

Changes

Path Traversal Protection for Template Gallery

Layer / File(s) Summary
Path safety utility and unit tests
apps/api/src/utils/path.ts, apps/api/src/utils/path.spec.ts
resolvePathWithinDir(baseDir, segment) safely resolves path segments within a base directory, returning null for traversal attempts, null bytes, or paths outside the boundary. Unit tests cover valid segments (including dots and spaces), escape attempts, and relative base directories.
Service integration with protection
apps/api/src/template/services/template-gallery/template-gallery.service.ts, apps/api/src/template/services/template-gallery/template-gallery.service.spec.ts
getTemplateById now resolves template cache paths via the utility; invalid paths log TEMPLATE_PATH_TRAVERSAL_BLOCKED and return null without reading files. Unit tests validate correct resolution for legitimate IDs and rejection of traversal attempts.
Functional end-to-end tests
apps/api/test/functional/templates.spec.ts
HTTP endpoint tests for GET /v1/templates/{id} assert that encoded traversal payloads (absolute escapes, double-encoding) return 404.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

✨ 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/api-templates-path-traversal

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

Copy link
Copy Markdown
Contributor

@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.

🧹 Nitpick comments (1)
apps/api/test/functional/templates.spec.ts (1)

34-35: ⚡ Quick win

Add explicit non-leak assertion for traversal responses.

A 404 alone doesn’t prove sensitive content is absent. Assert the response body does not contain leaked file markers.

Suggested test hardening
       const response = await app.request(`/v1/templates/${encodedId}`, {
         method: "GET",
         headers: new Headers({ "Content-Type": "application/json" })
       });

       expect(response.status).toBe(404);
+      const body = await response.text();
+      expect(body).not.toContain("root:x:");
+      expect(body).not.toContain("/etc/passwd");
🤖 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 `@apps/api/test/functional/templates.spec.ts` around lines 34 - 35, The test
currently only asserts response.status === 404; extend it to explicitly assert
the response body doesn't contain leaked file markers by adding a negative match
on the response payload (e.g. use response.text or response.body depending on
the test helper) immediately after expect(response.status).toBe(404);—for
example, assert response.text/body does not match a regex for common leak
indicators like /<\?xml|<!DOCTYPE|\/etc\/passwd|-----BEGIN|BEGIN|PRIVATE/i to
ensure no sensitive file contents are returned.
🤖 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.

Nitpick comments:
In `@apps/api/test/functional/templates.spec.ts`:
- Around line 34-35: The test currently only asserts response.status === 404;
extend it to explicitly assert the response body doesn't contain leaked file
markers by adding a negative match on the response payload (e.g. use
response.text or response.body depending on the test helper) immediately after
expect(response.status).toBe(404);—for example, assert response.text/body does
not match a regex for common leak indicators like
/<\?xml|<!DOCTYPE|\/etc\/passwd|-----BEGIN|BEGIN|PRIVATE/i to ensure no
sensitive file contents are returned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 824e81a8-5026-4c60-8db7-51dc966f01f8

📥 Commits

Reviewing files that changed from the base of the PR and between ef633f7 and 64d23cd.

📒 Files selected for processing (5)
  • apps/api/src/template/services/template-gallery/template-gallery.service.spec.ts
  • apps/api/src/template/services/template-gallery/template-gallery.service.ts
  • apps/api/src/utils/path.spec.ts
  • apps/api/src/utils/path.ts
  • apps/api/test/functional/templates.spec.ts

Copy link
Copy Markdown
Contributor

@stalniy stalniy left a comment

Choose a reason for hiding this comment

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

overcomplicated implementation but nice catch!

Comment on lines +248 to +252
const cachePath = resolvePathWithinDir(`${this.#galleriesCachePath}/v1/templates`, `${id}.json`);
if (!cachePath) {
this.#logger.warn({ event: "TEMPLATE_PATH_TRAVERSAL_BLOCKED", templateId: id });
return null;
}
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.

issue(blocking): seems like it's the wrong place to fix it. It also breaks encapsulation of #templateCachePath function.

To prevent path traversal it's enough to remove all slashes or block them on http schema (zod) level

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.48%. Comparing base (ef633f7) to head (64d23cd).
⚠️ Report is 14 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3242      +/-   ##
==========================================
- Coverage   66.68%   65.48%   -1.21%     
==========================================
  Files        1065      981      -84     
  Lines       26123    24038    -2085     
  Branches     6292     5881     -411     
==========================================
- Hits        17421    15742    -1679     
+ Misses       7609     7236     -373     
+ Partials     1093     1060      -33     
Flag Coverage Δ *Carryforward flag
api 84.91% <100.00%> (+0.23%) ⬆️
deploy-web 50.54% <ø> (ø) Carriedforward from ef633f7
log-collector ?
notifications 90.99% <ø> (ø) Carriedforward from ef633f7
provider-console 81.38% <ø> (ø) Carriedforward from ef633f7
provider-inventory ?
provider-proxy 86.08% <ø> (ø) Carriedforward from ef633f7
tx-signer ?

*This pull request uses carry forward flags. Click here to find out more.

Files with missing lines Coverage Δ
...vices/template-gallery/template-gallery.service.ts 94.48% <100.00%> (+0.99%) ⬆️
apps/api/src/utils/path.ts 100.00% <100.00%> (ø)

... and 93 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

2 participants