Skip to content

LANG-145: confine execution.python.path to the plugin directory - #189

Merged
RockChinQ merged 2 commits into
mainfrom
fix/lang-145-manifest-component-path
Oct 22, 2026
Merged

RockChinQ merged 2 commits into
mainfrom
fix/lang-145-manifest-component-path

Conversation

@RockChinQ

Copy link
Copy Markdown
Member

Context

execution.python.path is plugin-authored manifest data, so it is untrusted. The component loader joined it to the manifest's directory, stripped a trailing .py, converted separators to dots and imported — with sys.path.append(os.getcwd()) added on every call and never removed.

What I found when I tested it

The traversal the audit reported does not actually import outside the plugin. ../outside.py becomes <rel_dir>....outside, and the empty dotted segments raise ModuleNotFoundError; the same for ../../ and absolute paths (a leading / becomes a leading .). So there was no exploitable load-arbitrary-module path.

That means the behaviour rested on an accident of string replacement, and two real defects remained:

  1. no validation at all — any future refactor of that path handling could make it exploitable;
  2. the permanent sys.path.append(os.getcwd()) lets one plugin shadow modules for every plugin loaded afterwards.

Fix

_resolve_component_module resolves the target against the plugin directory and rejects:

  • empty / NUL path
  • absolute paths, drive letters
  • any .. component
  • anything whose resolved path leaves the plugin directory (commonpath check)

Import now inserts the plugin directory on sys.path only for the duration of the import and removes it in a finally, so a failure leaves sys.path untouched.

Tests

11 cases in tests/api/definition/test_manifest_component_path.py: every rejection form; a component inside the plugin still imports; a path without the .py suffix; sys.path byte-identical after both success and ModuleNotFoundError; module-cache isolation between cases.

tests/api   513 passed

RockChinQ and others added 2 commits October 22, 2026 14:07
execution.python.path is plugin-authored manifest data. The component loader
joined it to the manifest's directory, stripped a trailing .py, turned separators
into dots and imported the result, with sys.path.append(os.getcwd()) left behind
permanently.

The '..' traversal the audit described does not actually import outside the
plugin: '../x.py' becomes '<rel_dir>....x', whose empty dotted segments raise
ModuleNotFoundError. But the resolver had no validation at all, so the behaviour
depended on that accident, and the permanent cwd append let one plugin shadow
modules for every plugin loaded after it.

_resolve_component_module now resolves the target against the plugin directory
and rejects empty/NUL paths, absolute paths, drive letters, any '..' component,
and anything that resolves outside the plugin dir. Import is performed with the
plugin directory inserted on sys.path only for the duration of the import.

Tests: 11 cases - rejection forms, a component inside the plugin still importing,
the module path without a .py suffix, sys.path left byte-identical after both
success and ModuleNotFoundError, and module cache isolation between cases.

Co-authored-by: multica-agent <github@multica.ai>
… tests

Co-authored-by: multica-agent <github@multica.ai>
@RockChinQ
RockChinQ merged commit 9b2a1b4 into main Oct 22, 2026
8 checks passed
@RockChinQ
RockChinQ deleted the fix/lang-145-manifest-component-path branch October 22, 2026 14:20
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant