fix: path traversal in symlink_repo_index_service (#390)#403
Conversation
Reported by @umag: user-controlled names (model, workflow, vault, step, tag key, tag value) were passed directly into join() path construction without validation, allowing traversal outside the index directory. The tag key case was worst — the code already included ".." in the join before the user-controlled tagKey, amplifying the traversal. ## Fix Added a private assertPathContained() method (mirroring the identical pattern already in unified_data_repository.ts:995-1010) and applied it at every vulnerable path-construction site. ### Locations validated - indexModel() — modelName against models/ dir - handleModelDeleted() — event.modelName against models/ dir - indexWorkflow() — workflowName against workflows/ dir - handleWorkflowDeleted() — event.workflowName against workflows/ dir - indexWorkflowRun() — workflowName against workflows/ dir; each step.stepName against the steps/ dir - indexVault() — vaultName against vaults/ dir - handleVaultDeleted() — event.vaultName against vaults/ dir - indexTagBasedData() — tagKey against modelDir; tagValue against tagKeyDir ### Extra fix not in the original plan The try-catch in indexTagBasedData() previously swallowed all errors via logger.debug. Without a fix there, the new assertPathContained() calls inside that block would be dead code from a security standpoint. Added a re-throw guard so path traversal errors propagate: if (error instanceof Error && error.message.startsWith("Path traversal detected")) { throw error; } ## Tests added (symlink_repo_index_service_test.ts) 7 of the 9 planned traversal vectors covered with assertRejects: ✓ model name "../../../malicious" → handleModelCreated ✓ model name "../../../malicious" → handleModelDeleted ✓ workflow name "../../../malicious" → handleWorkflowCreated ✓ workflow name "../../../malicious" → handleWorkflowDeleted ✓ vault name "../../../malicious" → handleVaultCreated ✓ vault name "../../../malicious" → handleVaultDeleted ✓ step name "../../../malicious" → handleWorkflowRunStarted The 2 remaining plan vectors (tag key / tag value traversal) were not tested. They require a unifiedDataRepo stub; the existing test helper does not wire one up, and building a full UnifiedDataRepository stub is out of scope for this fix. ## Plan vs implementation delta | Area | Plan | Implemented | |---|---|---| | resolve import | ✓ | ✓ | | assertPathContained() method | ✓ | ✓ | | indexModel validation | ✓ | ✓ | | handleModelDeleted validation | ✓ | ✓ | | indexWorkflow validation | ✓ | ✓ | | handleWorkflowDeleted validation | ✓ | ✓ | | indexWorkflowRun validation | ✓ | ✓ | | indexVault validation | ✓ | ✓ | | handleVaultDeleted validation | ✓ | ✓ | | indexTagBasedData validation | ✓ | ✓ | | fix try-catch swallowing traversal error | ✗ not planned | ✓ added | | 7 path traversal tests | ✓ | ✓ | | tag key/value traversal tests | ✓ planned | ✗ omitted | Co-authored-by: Magistr <2269529+umag@users.noreply.github.com>
e99ca99 to
bb0c210
Compare
There was a problem hiding this comment.
✅ Approved
This is a well-crafted security fix for path traversal vulnerabilities in symlink_repo_index_service.ts. The implementation is solid and follows established patterns.
Code Quality Assessment
CLAUDE.md Compliance:
- ✓ TypeScript strict mode, no
anytypes - ✓ Named exports used throughout
- ✓ AGPLv3 license headers present
- ✓ Tests live next to source files (
symlink_repo_index_service_test.ts)
Security Fix Analysis:
- The
assertPathContained()method correctly usesresolve()to normalize paths before comparison - The check
!resolvedPath.startsWith(resolvedParent + "/")properly handles the slash to prevent partial directory name matches - The re-throw guard for path traversal errors in the
try-catchblock is a good catch - without it, the security validation would have been silently swallowed
Test Coverage:
- 7 of 9 planned attack vectors are covered with proper
assertRejectstests - The 2 missing tag key/value tests are documented and justified (require UnifiedDataRepository stub)
DDD Principles:
- Security validation is correctly placed at the infrastructure layer boundary where user-controlled data enters filesystem operations
- Follows the established pattern from
unified_data_repository.ts:995-1010
Suggestions (non-blocking)
-
Code duplication: The
assertPathContainedmethod now exists in bothunified_data_repository.tsandsymlink_repo_index_service.ts. A future refactoring could extract this to a shared path utilities module. However, duplicating security-critical code can be acceptable to ensure each component owns its validation logic. -
Consider adding tests for tag key/value traversal: While the current justification is valid, these vectors should be tested eventually if the test infrastructure allows.
LGTM - this properly addresses the security vulnerability reported in #390.
Fixes #390, reported by @umag.
Problem
symlink_repo_index_service.tspassed user-controlled names (model names, workflow names, vault names, step names, tag keys, tag values) directly intojoin()path construction without any validation, allowing path traversal outside the index directory. The tag key case was the worst — the source already included".."in the join before the user-controlledtagKey, amplifying the traversal.Fix
Added a private
assertPathContained()method (mirroring the identical pattern already inunified_data_repository.ts:995–1010) and applied it at every vulnerable path-construction site.Locations validated
indexModel()modelNamemodels/handleModelDeleted()event.modelNamemodels/indexWorkflow()workflowNameworkflows/handleWorkflowDeleted()event.workflowNameworkflows/indexWorkflowRun()workflowNameworkflows/indexWorkflowRun()step.stepNamesteps/indexVault()vaultNamevaults/handleVaultDeleted()event.vaultNamevaults/indexTagBasedData()tagKeymodelDirindexTagBasedData()tagValuetagKeyDirExtra fix beyond the original plan
The
try-catchinindexTagBasedData()previously swallowed all errors vialogger.debug. Without fixing this, the newassertPathContained()calls inside that block would be dead code from a security standpoint. Added a re-throw guard so path traversal errors propagate:Tests added
7 of the 9 planned traversal vectors covered with
assertRejectsinsymlink_repo_index_service_test.ts:../../../malicious→handleModelCreated../../../malicious→handleModelDeleted../../../malicious→handleWorkflowCreated../../../malicious→handleWorkflowDeleted../../../malicious→handleVaultCreated../../../malicious→handleVaultDeleted../../../malicious→handleWorkflowRunStartedThe 2 remaining planned vectors (tag key / tag value traversal) were not tested. They require a
unifiedDataRepostub; the existingcreateIndexServicetest helper does not wire one up, and building a fullUnifiedDataRepositorystub is out of scope for this fix.Plan vs implementation
resolveto importassertPathContained()methodindexModelvalidationhandleModelDeletedvalidationindexWorkflowvalidationhandleWorkflowDeletedvalidationindexWorkflowRunvalidationindexVaultvalidationhandleVaultDeletedvalidationindexTagBasedDatatag key/value validation