Skip to content

security(powerpoint): importlib code execution in build_deck.py content-extra.py loading #1017

@WilliamBerryiii

Description

@WilliamBerryiii

Summary

build_deck.py loads and executes arbitrary Python from content-extra.py files via importlib.util.spec_from_file_location() without any validation. A malicious or careless content-extra.py can import dangerous stdlib modules (e.g., subprocess, os, shutil) or call dangerous builtins (eval, exec, __import__, compile, breakpoint) to gain full code execution in the build environment.

CWE: CWE-94 – Improper Control of Generation of Code ('Code Injection')
Severity: CRITICAL

Acceptance Criteria

  • content-extra.py scripts are validated against a known-safe allowlist before importlib execution
  • Blocked stdlib modules (subprocess, os, shutil, ctypes, etc.) raise a clear error with the module name
  • Dangerous builtins (eval, exec, __import__, compile, breakpoint) are detected and rejected
  • Indirect bypass builtins (getattr, setattr, delattr, globals, locals, vars) are detected and rejected
  • Safe imports (pptx, pathlib, copy, math, json, etc.) continue to work
  • --allow-scripts CLI flag opts in to skipping validation for trusted content
  • Runtime namespace restriction strips dangerous builtins from executed module
  • Security documentation added to SKILL.md
  • CLI integration tests verify --allow-scripts propagates through main()
  • Unit tests cover blocked imports, dangerous builtins, indirect bypass builtins, namespace restriction, valid scripts, and edge cases

Implementation Details

AST-based validation (_validate_content_extra())

Parses content-extra.py with ast.parse() before execution and walks the AST to check:

  • Import statements (import X, from X import Y): top-level module must be in pptx.* or a safe subset of stdlib. 30 blocked stdlib modules in _BLOCKED_STDLIB_MODULES. Third-party packages are rejected.
  • Dangerous builtins (_DANGEROUS_BUILTINS): calls to eval, exec, __import__, compile, breakpoint are rejected.
  • Indirect bypass builtins (_INDIRECT_BYPASS_BUILTINS): calls to getattr, setattr, delattr, globals, locals, vars are rejected to prevent getattr(builtins, 'exec') style bypasses.
  • Syntax errors: scripts that fail to parse are rejected before execution.

Runtime namespace restriction

Even after AST validation passes, the executed module's __builtins__ dict is restricted to safe builtins only. Dangerous builtins (eval, exec, compile, breakpoint) and indirect bypass builtins (getattr, setattr, delattr, globals, locals, vars) are stripped from the namespace before spec.loader.exec_module() runs. __import__ is kept because the import machinery requires it; the AST checker blocks direct __import__() calls.

--allow-scripts opt-in flag

build_slide() accepts a keyword-only allow_scripts: bool = False parameter. When True, both AST validation and runtime namespace restriction are skipped — intended for trusted content scenarios. The CLI exposes this as --allow-scripts, threaded through all three build_slide() call sites in main().

Security documentation (SKILL.md)

A "Security Validation" subsection was added under the "Complex Drawings (content-extra.py)" section documenting:

  • Allowed and blocked imports
  • Blocked builtins (dangerous and indirect bypass)
  • Runtime namespace restriction behavior
  • --allow-scripts flag usage with CLI example
  • Error behavior (ContentExtraError)

Audit findings

No other skills in the repository use importlib for dynamic code execution. export_slides.py uses subprocess.run for controlled tool invocation (not user-controlled dynamic code).

Test coverage

119 tests pass (was 90 before this work). New tests cover:

  • Blocked stdlib modules (subprocess, os, shutil, socket, ctypes)
  • From-import syntax (from os import path)
  • Third-party imports (e.g., requests)
  • Dangerous builtins (eval, exec, import, compile, breakpoint)
  • Indirect bypass builtins (delattr, getattr, globals, locals, setattr, vars)
  • Syntax errors in content-extra scripts
  • Valid pptx and safe stdlib imports pass through
  • build_slide() integration: valid content-extra executes, invalid is rejected
  • allow_scripts=True bypasses validation
  • allow_scripts=False still validates
  • Runtime namespace blocks eval via __builtins__ dict
  • Runtime namespace allows safe builtins (len, range, str)
  • CLI integration: --allow-scripts flag propagates through main() to build_slide()
  • CLI integration: without --allow-scripts, blocked imports fail through main()

Files changed

File Changes
scripts/build_deck.py Added import ast, import builtins, _BLOCKED_STDLIB_MODULES, _DANGEROUS_BUILTINS, _INDIRECT_BYPASS_BUILTINS, ContentExtraError, _check_module_allowed(), _validate_content_extra(), allow_scripts parameter, runtime namespace restriction, --allow-scripts CLI argument
tests/test_build_deck.py Added TestContentExtraValidation (18 tests) and TestAllowScriptsCLI (2 integration tests)
SKILL.md Added "Security Validation" subsection with allowlist/blocklist docs, namespace restriction, --allow-scripts usage

Metadata

Metadata

Labels

bugSomething isn't workingsecuritySecurity-related changes or concernsskillsCopilot skill packages (SKILL.md)

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions