T09 · Insecure Skill Coding Practices
- Location
run.py:33- Finding
Path Traversal Allows Execution of an Unintended Python Handler
- Content
View full analysis
Vulnerability Details
File Location:
run.py, lines 33-43
Vulnerability Type: Path traversal in an executable file dispatcher
Risk Level: MediumVulnerable Code
python subskill = sys.argv[1] handler = os.path.join(SKILL_ROOT, "sub-skills", subskill, "scripts", "handler.py") if not os.path.isfile(handler): print(f"Error: subskill '{subskill}' was not found: {handler}", file=sys.stderr) sys.exit(1) result = subprocess.run( [sys.executable, handler] + sys.argv[2:], cwd=SKILL_ROOT, )Technical Analysis
The dispatcher incorporates the user-controlled
subskillargument directly into the path of a Python file that it subsequently executes. It only verifies that the resulting path refers to a file; it does not:- Restrict the argument to the packaged sub-skill names.
- Reject absolute paths or
..traversal components. - Canonicalize the resulting path before validation.
- Verify that the resolved handler remains inside the intended
sub-skillsdirectory. - Protect against a symlink resolving outside that directory.
Consequently, a traversal value can cause the path to resolve outside the project’s
sub-skillsdirectory. If the resulting external directory containsscripts/handler.py, that file will be executed with the current Python interpreter.The list-form
subprocess.run()invocation does prevent shell metacharacter injection becauseshell=Trueis not used. However, that protection does not prevent selection and execution of an unintended Python script through path traversal.Attack Path
- An attacker gains control of, or can influence, the first argument supplied to
run.py. - The attacker identifies or arranges an accessible directory outside the intended
sub-skillsdirectory containing a file atscripts/handler.py. - The attacker supplies a traversal-based sub-skill value, such as a sequence containing
../, that causes the co ...[truncated 1466 chars]
- Remediation
View remediation
Remediation Suggestions
- Define an explicit allowlist containing the 16 packaged sub-skill names and reject every other value before constructing a path.
- Reject absolute paths, path separators,
.components, and..components in the supplied name. - Resolve both the expected sub-skills directory and candidate handler with
os.path.realpath(). - Use
os.path.commonpath()to confirm that the canonical handler remains under the canonical sub-skills directory. - Validate the canonical path after resolving symlinks so a symlink cannot redirect execution outside the package.
- Where possible, build a static mapping from each accepted sub-skill name to its exact handler path rather than treating the argument as a path component.
- Add negative tests covering traversal strings, absolute paths, nested path separators, and symlink escapes.
Example hardening approach:
python ALLOWED_SUBSKILLS = { "economic-china-cpi-monthly", "economic-china-credit-loans-monthly", "economic-china-customs-trade-monthly", "economic-china-fiscal-revenue-monthly", "economic-china-fixed-asset-investment-monthly", "economic-china-forex-gold-monthly", "economic-china-gdp-quarterly", "economic-china-industrial-added-value-monthly", "economic-china-lpr-monthly", "economic-china-money-supply-monthly", "economic-china-pmi-monthly", "economic-china-ppi-monthly", "economic-china-reserve-ratio-monthly", "economic-china-retail-sales-monthly", "economic-china-tax-revenue-monthly", "economic-us-economic-by-type", } if subskill not in ALLOWED_SUBSKILLS: raise SystemExit("Invalid subskill") subskills_root = os.path.realpath(os.path.join(SKILL_ROOT, "sub-skills")) handler = os.path.realpath( os.path.join(subskills_root, subskill, "scripts", "handler.py") ) if ( os.path.commonpath([subskills_root, handler]) != subskills_root or not os.pa ...[truncated 76 chars]
