Skip to content

fix(tm-core): slugify tag before using it in file paths (CWE-23 path traversal)#1712

Open
zied-jlassi wants to merge 2 commits into
eyaltoledano:mainfrom
zied-jlassi:fix/tag-path-traversal
Open

fix(tm-core): slugify tag before using it in file paths (CWE-23 path traversal)#1712
zied-jlassi wants to merge 2 commits into
eyaltoledano:mainfrom
zied-jlassi:fix/tag-path-traversal

Conversation

@zied-jlassi

@zied-jlassi zied-jlassi commented Jun 22, 2026

Copy link
Copy Markdown

What type of PR is this?

  • 🐛 Bug fix

Description

tag is interpolated raw into filenames in two tm-core path builders:

  • ComplexityReportManager.getReportPathtask-complexity-report_<tag>.json (read)
  • TaskFileGeneratorService.getTaskFileNametask_<id>_<tag>.md (write)

Tags are user/agent/config controllable (CLI --tag, MCP tag argument,
.taskmaster/state.json currentTag, TASKMASTER_TAG), and none of these entry
points validates the value on the read/generate path (validateTagName only runs
on create/rename/copy). So a tag like ../../../../../escape/x escapes
.taskmaster/ — an out-of-project file read (complexity report) and write
(task .md). CWE-22/23/73.

This is a parity regression: the legacy JS path slugified the tag
(slugifyTagForFilePath in scripts/modules/utils.js); the TypeScript rewrite
dropped it, and the sanitizeTag helper on the storage interface was never wired in.

Fix: a shared slugifyTagForFilePath helper (mirroring the legacy behavior),
applied at both path builders and at cleanupOrphanedFiles so written names and
the orphan-cleanup pattern stay in sync. Sanitizing at the path-building chokepoint
covers all three entry vectors at once. Legitimately named tags (already
^[a-zA-Z0-9_-]+$) are unaffected beyond lowercasing. Honest severity: Medium
the extension is forced (.md/.json), so this is not arbitrary-file overwrite or RCE.

Related Issues

Fixes #1711.

How to Test This

npm run test -w @tm/core -- tag-path.spec.ts task-file-generator.service.spec.ts

Expected result: all tests pass, including the new negative-oracle cases where
../../x, ..\..\x, NUL and ..%2f.. are neutralized while feature-v2 is preserved.

Manual before/after (project root /work/project): tag ../../../../../escape/secret
resolved outside the project before the fix and resolves to
…/.taskmaster/reports/task-complexity-report_escape-secret.json (contained) after.

Contributor Checklist

Changelog Entry

Fix path traversal via an unsanitized tag in the complexity-report reader and task-file generator (tags are now slugified before use in file paths).


Disclosure: this fix was AI-assisted and human-reviewed; it was tested on the repo's
own vitest/tsc/biome before submission. Happy to follow whatever disclosure format you prefer.

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Patched a security vulnerability where crafted tags could be interpolated into report/task filenames, enabling path traversal.
    • Tags are now converted into filesystem-safe, lowercased slugs before being used in filenames, preventing ../, slashes, and other unsafe characters from escaping the intended directory.
  • Tests
    • Added coverage to verify safe slug generation, expected fallback behavior, and protection against traversal-style inputs.

The complexity-report reader (ComplexityReportManager.getReportPath) and the
per-task file generator (TaskFileGeneratorService) interpolated the `tag` value
straight into a filename. Tags are user/agent/config controllable (CLI `--tag`,
MCP `tag` argument, `.taskmaster/state.json` `currentTag`, `TASKMASTER_TAG`), so a
tag such as `../../../../../escape/x` escaped the `.taskmaster/` directory — an
out-of-project file read (complexity report) and write (task markdown).

The legacy JS path slugified the tag (`slugifyTagForFilePath`), but the
TypeScript rewrite dropped that step, and the `sanitizeTag` helper on the storage
interface was never wired in. This adds a shared `slugifyTagForFilePath` helper
(mirroring the legacy behavior) and applies it at both path builders, plus the
orphan-cleanup pattern so writes and cleanup stay in sync. Legitimately named
tags (which already match `^[a-zA-Z0-9_-]+$`) are unaffected.

Adds unit tests with a negative oracle (`../`, `..\`, NUL, URL-encoded must be
neutralized; `feature-v2` preserved) and a changeset.

Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cdc59a1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
task-master-ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review 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: Pro

Run ID: 07d89be1-b77a-46db-b4ef-c43173d14001

📥 Commits

Reviewing files that changed from the base of the PR and between 441e37c and cdc59a1.

📒 Files selected for processing (2)
  • packages/tm-core/src/common/utils/tag-path.spec.ts
  • packages/tm-core/src/common/utils/tag-path.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/tm-core/src/common/utils/tag-path.spec.ts
  • packages/tm-core/src/common/utils/tag-path.ts

📝 Walkthrough

Walkthrough

A new slugifyTagForFilePath utility is added to tm-core that converts untrusted tag strings into filesystem-safe slugs. This utility is applied at both path-building sinks—ComplexityReportManager.getReportPath and TaskFileGeneratorService.getTaskFileName/cleanupOrphanedFiles—replacing previously raw tag interpolation. Tests and a changeset entry are included.

Changes

Tag Path Traversal Security Fix

Layer / File(s) Summary
slugifyTagForFilePath utility, export, and tests
packages/tm-core/src/common/utils/tag-path.ts, packages/tm-core/src/common/utils/tag-path.spec.ts, packages/tm-core/src/common/utils/index.ts, .changeset/fix-tag-path-traversal.md
Defines slugifyTagForFilePath with a MAX_TAG_SLUG_LENGTH constant; the pipeline replaces unsafe characters with hyphens, collapses/trims hyphens, lowercases, and truncates. Returns 'unknown-tag' for empty/nullish input or when slugification collapses to empty. Re-exported from the utils index. Vitest suite covers valid tag preservation, lowercasing, traversal-style negative oracles (../../, \, NUL, etc.), hyphen normalization, max length, and nullish/collapsed fallback. Changeset documents the fix aligning with legacy JS behavior.
Apply slugification at read/write sinks
packages/tm-core/src/modules/reports/managers/complexity-report-manager.ts, packages/tm-core/src/modules/tasks/services/task-file-generator.service.ts
ComplexityReportManager.getReportPath now calls slugifyTagForFilePath instead of embedding the raw tag in the .json filename. TaskFileGeneratorService.getTaskFileName uses the slugified tag in the .md filename, and cleanupOrphanedFiles constructs its cleanup regex from the same slugified value to maintain consistency.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • Crunchyman-ralph
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: fixing a path traversal vulnerability (CWE-23) by slugifying tags before using them in file paths, which directly aligns with the primary objective.
Linked Issues check ✅ Passed All core objectives from issue #1711 are met: a shared slugifyTagForFilePath helper is created, applied at both path-building chokepoints, slugification handles edge cases (empty results, non-string input), and comprehensive tests validate path traversal sequences are neutralized.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the path traversal vulnerability: tag-aware utilities, test coverage for slug behavior, and integration at the two affected file path builders with no unrelated modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/tm-core/src/common/utils/tag-path.spec.ts (1)

63-71: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add test for all-invalid-char input.

The edge case where tagName contains only invalid characters (e.g., "../../../") should be tested to ensure it returns the fallback rather than an empty string (if the implementation fix is applied).

Suggested test case
 	describe('empty / invalid input', () => {
 		it('returns a fallback for empty string', () => {
 			expect(slugifyTagForFilePath('')).toBe('unknown-tag');
 		});

 		it('returns a fallback for null/undefined', () => {
 			expect(slugifyTagForFilePath(null)).toBe('unknown-tag');
 			expect(slugifyTagForFilePath(undefined)).toBe('unknown-tag');
 		});
+
+		it('returns a fallback when all characters are invalid', () => {
+			expect(slugifyTagForFilePath('../../../')).toBe('unknown-tag');
+			expect(slugifyTagForFilePath('///')).toBe('unknown-tag');
+		});
 	});
🤖 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 `@packages/tm-core/src/common/utils/tag-path.spec.ts` around lines 63 - 71, Add
a test case in the "empty / invalid input" describe block for the
slugifyTagForFilePath function to verify that when tagName contains only invalid
characters (such as "../../../"), the function returns the fallback value
"unknown-tag" instead of an empty string. This test should be placed alongside
the existing tests for empty string and null/undefined inputs to ensure all edge
cases are covered.
🤖 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.

Inline comments:
In `@packages/tm-core/src/common/utils/tag-path.ts`:
- Around line 42-51: After the series of string transformations (replace, remove
leading/trailing hyphens, collapse, lowercase, substring) that process the
tagName, add a final validation check to handle the edge case where all
transformations result in an empty string. If the final result is empty, return
the fallback value 'unknown-tag', otherwise return the processed result. This
prevents malformed filenames when the input consists entirely of invalid
characters.

---

Nitpick comments:
In `@packages/tm-core/src/common/utils/tag-path.spec.ts`:
- Around line 63-71: Add a test case in the "empty / invalid input" describe
block for the slugifyTagForFilePath function to verify that when tagName
contains only invalid characters (such as "../../../"), the function returns the
fallback value "unknown-tag" instead of an empty string. This test should be
placed alongside the existing tests for empty string and null/undefined inputs
to ensure all edge cases are covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 76c252a4-e64f-4315-b244-9fbb9e718726

📥 Commits

Reviewing files that changed from the base of the PR and between c0c98d3 and 441e37c.

📒 Files selected for processing (6)
  • .changeset/fix-tag-path-traversal.md
  • packages/tm-core/src/common/utils/index.ts
  • packages/tm-core/src/common/utils/tag-path.spec.ts
  • packages/tm-core/src/common/utils/tag-path.ts
  • packages/tm-core/src/modules/reports/managers/complexity-report-manager.ts
  • packages/tm-core/src/modules/tasks/services/task-file-generator.service.ts

Comment thread packages/tm-core/src/common/utils/tag-path.ts
Addresses CodeRabbit review on eyaltoledano#1712: a tag composed entirely of invalid
characters (e.g. `../../../`) collapses to an empty slug, which would produce a
malformed `task-complexity-report_.json` suffix. Return the same `unknown-tag`
sentinel already used for empty/non-string input, and cover it with tests.

Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
@zied-jlassi

Copy link
Copy Markdown
Author

Good catch on the empty-slug edge case — a tag that is entirely invalid characters (e.g. ../../../) collapsed to "" and would have produced a malformed task-complexity-report_.json suffix. Fixed in cdc59a1: the helper now returns the same unknown-tag sentinel used for empty/non-string input, with tests covering ../../../ and ////.

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.

Path traversal via unsanitized tag in tm-core file path builders (CWE-23) — regression vs legacy slugify

1 participant