Bug Description
Event-hook script resolution joins the command template's first scripts: token onto the extension or project base with Path / token. pathlib drops the base when that token is an absolute path, and there is no resolve() plus relative_to(base) check. A command template can therefore run a host binary (or a .. walk out of .specify/) instead of a script under the extension or project tree.
The same join is used in two places:
- CLI resolver
_resolve_event_command_argv in src/specify_cli/events.py
- Generated dispatcher
_resolve_argv (stdlib fallback copied into the same file)
Sibling install paths already confine user-controlled paths (test_registrar_path_traversal.py, test_extension_add_path_traversal.py). Event dispatch does not.
Steps to Reproduce
- Check out bf88c9f9a82f.
- In a temp project, write a command template whose
scripts.sh first token is an absolute path that exists on the host (for example /bin/echo) plus a harmless argument.
- Call
_resolve_event_command_argv(template, project_root, None) (or specify event run after installing a command that maps to that template).
- Observe that argv starts at the host binary, not under
.specify/.
Minimal illustration of the join (no network, no install):
from pathlib import Path
base = Path("/tmp/proj/.specify")
print(base / "/bin/echo") # PosixPath('/bin/echo'); base is discarded
Expected Behavior
The first scripts: token should resolve under the documented base (.specify/ or .specify/extensions/<id>/). Absolute paths and resolved paths that escape that base should return no argv (same degrade-to-None policy used for malformed scripts: values).
Actual Behavior
base / tokens[0] yields the absolute host path when the token is absolute. If that path exists, the event runner executes it with subprocess.run(..., shell=False).
Specify CLI Version
0.16.5.dev0 (main at bf88c9f9a82f)
AI Agent
Grok Build
Operating System
macOS 26.6.1
Python Version
Python 3.14.7
Additional Context
Installing an extension already means trusting the scripts it ships. This is a confinement gap: a one-line scripts: frontmatter can invoke a host binary without a reviewable file under the extension tree.
Suggested fix: after shlex.split, reject an absolute first token; resolve() the joined path and require it to be relative to base.resolve(). Cover both the CLI resolver and the generated dispatcher. I can open a PR with tests if maintainers want that.
Posted on behalf of @SebTardif by Grok (model: grok-4.6).
Bug Description
Event-hook script resolution joins the command template's first
scripts:token onto the extension or project base withPath / token. pathlib drops the base when that token is an absolute path, and there is noresolve()plusrelative_to(base)check. A command template can therefore run a host binary (or a..walk out of.specify/) instead of a script under the extension or project tree.The same join is used in two places:
_resolve_event_command_argvinsrc/specify_cli/events.py_resolve_argv(stdlib fallback copied into the same file)Sibling install paths already confine user-controlled paths (
test_registrar_path_traversal.py,test_extension_add_path_traversal.py). Event dispatch does not.Steps to Reproduce
scripts.shfirst token is an absolute path that exists on the host (for example/bin/echo) plus a harmless argument._resolve_event_command_argv(template, project_root, None)(orspecify event runafter installing a command that maps to that template)..specify/.Minimal illustration of the join (no network, no install):
Expected Behavior
The first
scripts:token should resolve under the documented base (.specify/or.specify/extensions/<id>/). Absolute paths and resolved paths that escape that base should return no argv (same degrade-to-None policy used for malformedscripts:values).Actual Behavior
base / tokens[0]yields the absolute host path when the token is absolute. If that path exists, the event runner executes it withsubprocess.run(..., shell=False).Specify CLI Version
0.16.5.dev0 (main at bf88c9f9a82f)
AI Agent
Grok Build
Operating System
macOS 26.6.1
Python Version
Python 3.14.7
Additional Context
Installing an extension already means trusting the scripts it ships. This is a confinement gap: a one-line
scripts:frontmatter can invoke a host binary without a reviewable file under the extension tree.Suggested fix: after
shlex.split, reject an absolute first token;resolve()the joined path and require it to be relative tobase.resolve(). Cover both the CLI resolver and the generated dispatcher. I can open a PR with tests if maintainers want that.Posted on behalf of @SebTardif by Grok (model: grok-4.6).