Skip to content

security: replace unrestricted setattr with allowlist in Python backend#28083

Open
titaiwangms wants to merge 9 commits intomainfrom
fix/backend-setattr-security-allowlist
Open

security: replace unrestricted setattr with allowlist in Python backend#28083
titaiwangms wants to merge 9 commits intomainfrom
fix/backend-setattr-security-allowlist

Conversation

@titaiwangms
Copy link
Copy Markdown
Contributor

Summary

Fixes a critical security vulnerability in the ONNX Runtime Python backend where user-controlled kwargs were applied to SessionOptions and RunOptions via unrestricted setattr(), allowing arbitrary file overwrites.

Vulnerability

The prepare() method in onnxruntime/python/backend/backend.py iterated over user-controlled kwargs and used setattr() to apply them directly to a SessionOptions instance. The hasattr() check was not a security guard — it returned True for all exposed properties including dangerous ones like optimized_model_filepath.

Attack vector:

onnxruntime.backend.prepare(
    model_path,
    optimized_model_filepath="/etc/passwd",  # overwrites any file with protobuf binary
    graph_optimization_level=onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
)

The same pattern existed in backend_rep.py for RunOptions.

Fix

Replaced the unrestricted hasattr/setattr loop in both files with strict allowlists:

  • _ALLOWED_SESSION_OPTIONS (13 safe attrs) in backend.py
  • _ALLOWED_RUN_OPTIONS (4 safe attrs) in backend_rep.py

Blocked-but-known SessionOptions attributes raise a clear RuntimeError. RunOptions silently ignores non-allowlisted keys because run_model() forwards the same kwargs to both paths.

Blocked dangerous attributes:

  • optimized_model_filepath — triggers Model::Save(), overwrites arbitrary files with protobuf binary
  • profile_file_prefix — writes profiling JSON to arbitrary path
  • enable_profiling — causes uncontrolled file writes to cwd
  • terminate (RunOptions) — denies the current inference call

Tests

Added TestBackendKwargsAllowlist with 7 tests covering all 3 exploit vectors (blocked + raise), safe allowlisted attrs (accepted), unknown attrs (silently ignored), and RunOptions blocked attrs (silently ignored). All 9 tests pass (7 new + 2 pre-existing), no regressions.

Files Changed

  • onnxruntime/python/backend/backend.py
  • onnxruntime/python/backend/backend_rep.py
  • onnxruntime/test/python/onnxruntime_test_python_backend.py

titaiwangms and others added 5 commits April 15, 2026 18:34
In backend.py and backend_rep.py, user-controlled kwargs were applied to
SessionOptions/RunOptions via unrestricted setattr(), allowing arbitrary
file overwrites via optimized_model_filepath (MSRC reported vulnerability).

Fix:
- Add _ALLOWED_SESSION_OPTIONS frozenset (13 safe properties) in backend.py
- Add _ALLOWED_RUN_OPTIONS frozenset (4 safe properties) in backend_rep.py
- Replace hasattr/setattr loops with allowlist checks
- backend.py: blocked-but-known SessionOptions attrs raise RuntimeError;
  truly unknown attrs silently ignored (may be RunOptions keys from run_model)
- backend_rep.py: all non-allowlisted attrs silently ignored;
  run_model() forwards same kwargs to both prepare() and run()

Dangerous attributes now blocked: optimized_model_filepath,
profile_file_prefix, enable_profiling, terminate, training_mode

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add TestBackendKwargsAllowlist class to onnxruntime_test_python_backend.py
covering the setattr allowlist fix in backend.py and backend_rep.py:

- test_blocked_session_option_optimized_model_filepath_raises: verifies
  optimized_model_filepath is blocked with RuntimeError (file overwrite exploit)
- test_blocked_session_option_profile_file_prefix_raises: verifies
  profile_file_prefix is blocked with RuntimeError (profiling file write)
- test_unknown_kwarg_is_silently_ignored: verifies truly unknown kwargs
  are silently ignored and inference still produces correct output
- test_safe_session_option_graph_optimization_level_is_accepted: verifies
  allowlisted graph_optimization_level kwarg is accepted and works
- test_safe_session_option_intra_op_num_threads_is_accepted: verifies
  allowlisted intra_op_num_threads kwarg is accepted and works
- test_blocked_run_option_terminate_is_silently_ignored: verifies that
  blocked RunOptions attrs are silently ignored in rep.run() so that
  run_model() kwargs forwarding does not raise

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover the third dangerous SessionOptions attribute excluded from
_ALLOWED_SESSION_OPTIONS: enable_profiling causes uncontrolled profiling
JSON file writes to cwd and must raise RuntimeError when passed as a kwarg
to backend.prepare().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- backend.py line 136: reuse existing `options` object in hasattr check instead
  of constructing a new SessionOptions() C++ object on each non-allowlisted kwarg
- backend.py prepare() docstring: clarify that only a safe subset of
  SessionOptions attrs are accepted, referencing _ALLOWED_SESSION_OPTIONS
- backend.py run_model() docstring: clarify that only a safe subset of
  RunOptions attrs are accepted, referencing _ALLOWED_RUN_OPTIONS
- backend_rep.py: expand _ALLOWED_RUN_OPTIONS comment to explain WHY
  'terminate' and 'training_mode' are excluded
- test file: remove setattr() implementation detail from TestBackendKwargsAllowlist
  class docstring; describe behavior not implementation

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Captures the allowlist pattern used to fix the unrestricted setattr()
vulnerability in onnxruntime/python/backend/. Documents the insecure
hasattr/setattr pattern, the correct frozenset allowlist fix, key
conventions (RuntimeError, object reuse, silent ignore for forwarded
kwargs), and the dangerous SessionOptions properties that must never
be allowlisted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms requested a review from Copilot April 15, 2026 19:55
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can commit the suggested changes from lintrunner.

Comment thread onnxruntime/python/backend/backend.py Outdated
Comment thread onnxruntime/python/backend/backend_rep.py Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Hardens the ONNX Runtime Python backend by preventing user-controlled kwargs from setting unsafe SessionOptions / RunOptions attributes via unrestricted setattr(), mitigating arbitrary file write and inference disruption vectors.

Changes:

  • Added strict allowlists for SessionOptions (raise on known-but-blocked attributes) and RunOptions (silently ignore non-allowlisted keys).
  • Updated backend API docstrings to document allowlisted behavior.
  • Added regression tests covering blocked attributes, allowed attributes, and ignore behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
onnxruntime/python/backend/backend.py Introduces _ALLOWED_SESSION_OPTIONS and raises on blocked known SessionOptions attributes.
onnxruntime/python/backend/backend_rep.py Introduces _ALLOWED_RUN_OPTIONS and ignores all non-allowlisted RunOptions keys.
onnxruntime/test/python/onnxruntime_test_python_backend.py Adds tests validating allowlist, block/raise semantics, and ignore behavior.
.github/skills/python-kwargs-setattr-security/SKILL.md Documents the insecure pattern and the repo’s allowlist-based remediation approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread onnxruntime/python/backend/backend.py
Comment thread onnxruntime/test/python/onnxruntime_test_python_backend.py
Comment thread onnxruntime/test/python/onnxruntime_test_python_backend.py Outdated
- backend.py: use ', '.join(sorted(...)) in RuntimeError message for
  human-readable comma-separated list of allowed attributes
- test file: wrap TestBackendKwargsAllowlist docstring to fit 120 chars
  (lintrunner/ruff formatting requirement)

Note: frozenset formatting (frozenset({...}) -> frozenset({...}) indented)
was already correctly applied in prior commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@vraspar
Copy link
Copy Markdown
Contributor

vraspar commented Apr 15, 2026

Review: PR #28083 — security: replace unrestricted setattr with allowlist in Python backend

Thanks for the security fix — the core approach (frozenset allowlists replacing unrestricted hasattr/setattr) is correct and I don't see a bypass within the backend kwargs path. A few items to address before merge:


1. RunOptions.terminate becomes a silent no-op (behavioral regression)

Before this PR, rep.run(..., terminate=True) and backend.run_model(..., terminate=True) actually worked — terminate is a real, exposed RunOptions attribute. After this PR it's silently swallowed.

The silent-ignore design is driven by run_model() forwarding the same kwargs to both prepare() and rep.run(), but that's an implementation detail that shouldn't dictate the API contract. Consider splitting kwargs in run_model() into session-options vs run-options buckets so each path only receives its own keys. That way RunOptions can raise on blocked attrs (like terminate) instead of silently dropping them.

2. No run_model() end-to-end test

The PR justifies the asymmetric error handling (raise vs. silent ignore) based on run_model() forwarding kwargs to both paths, but no test actually exercises backend.run_model(...). Suggest adding tests for:

  • run_model(..., graph_optimization_level=...) — safe SessionOptions kwarg
  • run_model(..., only_execute_path_to_fetches=...) — safe RunOptions kwarg
  • run_model(..., terminate=True) — whatever the intended behavior ends up being

3. SKILL.md is in the wrong directory

ORT's existing skills live under .agents/skills/ (ort-build, ort-lint, ort-test). This PR adds .github/skills/ which doesn't exist on main. I'd suggest either moving it to .agents/skills/ or dropping it from this PR and adding it separately after maintainer buy-in — it widens a focused security fix with repo-tooling content.

4. Minor: error message content not asserted in tests

The tests check that RuntimeError is raised for blocked attrs, but don't assert on the message content. A quick assertIn("not permitted", str(ctx.exception)) would guard against accidental regressions in the user-facing error message.

5. Consider an allowlist-drift regression test

A test that enumerates the current writable properties on SessionOptions / RunOptions and asserts they equal allowed ∪ blocked would catch future pybind additions that silently fall through without explicit review.


Overall this is a solid, well-scoped security fix. Items 1-3 are the actionable ones; 4-5 are nice-to-haves.

titaiwangms and others added 2 commits April 15, 2026 23:27
- backend.py: import _ALLOWED_RUN_OPTIONS from backend_rep; split kwargs
  in run_model() so SessionOptions and RunOptions each receive only their
  own keys — fixes silent swallowing of RunOptions attrs like terminate
- backend_rep.py: update run() loop to raise RuntimeError on blocked-but-known
  RunOptions attrs (same pattern as prepare()); remove stale comment about
  forwarding from run_model()
- tests: rename test_blocked_run_option_terminate_is_silently_ignored to
  test_blocked_run_option_terminate_raises (behavior changed); add
  assertIn('not permitted') to 3 blocked-attr tests; add 3 new run_model()
  end-to-end tests covering safe session option, safe run option, and
  blocked run option
- skill: move SKILL.md from .github/skills/ to .agents/skills/ (correct
  location matching ort-build, ort-lint, ort-test)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skills in this repo live under .agents/skills/ (alongside ort-build,
ort-lint, ort-test). Remove the misplaced .github/skills/ copy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms
Copy link
Copy Markdown
Contributor Author

Thanks @vraspar — all items addressed in the latest commits:

Item 1 (kwargs split + RunOptions raise): run_model() now splits kwargs into session_kwargs and run_kwargs before passing to prepare() and rep.run() respectively. With kwargs properly separated, backend_rep.py's run() now raises RuntimeError on blocked-but-known RunOptions attrs (same pattern as prepare()) — so terminate=True raises rather than silently drops.

Item 2 (run_model tests): Added 3 new tests: test_run_model_with_safe_session_option, test_run_model_with_safe_run_option, and test_run_model_with_blocked_run_option_raises. Also renamed test_blocked_run_option_terminate_is_silently_ignoredtest_blocked_run_option_terminate_raises to match the new behavior.

Item 3 (SKILL.md location): Moved from .github/skills/ to .agents/skills/ to match ORT conventions.

Item 4 (error message assertions): Added assertIn('not permitted', str(ctx.exception)) to the 3 existing blocked-attr tests.

Replace hardcoded /tmp paths with proper tempfile usage:
- optimized_model_filepath test: uses tempfile.NamedTemporaryFile
- profile_file_prefix test: uses tempfile.TemporaryDirectory + os.path.join
Add os and tempfile imports at the top of the test file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (73ab72e7) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can commit the suggested changes from lintrunner.


from onnxruntime import InferenceSession, SessionOptions, get_available_providers, get_device
from onnxruntime.backend.backend_rep import OnnxRuntimeBackendRep
from onnxruntime.backend.backend_rep import OnnxRuntimeBackendRep, _ALLOWED_RUN_OPTIONS
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
from onnxruntime.backend.backend_rep import OnnxRuntimeBackendRep, _ALLOWED_RUN_OPTIONS
from onnxruntime.backend.backend_rep import _ALLOWED_RUN_OPTIONS, OnnxRuntimeBackendRep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants