Skip to content

[Bug]: Event hook scripts: paths are not confined to the extension or project base #4131

Description

@SebTardif

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

  1. Check out bf88c9f9a82f.
  2. 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.
  3. Call _resolve_event_command_argv(template, project_root, None) (or specify event run after installing a command that maps to that template).
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions