Skip to content

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

Merged
titaiwangms merged 6 commits intomainfrom
fix/backend-setattr-security-allowlist
Apr 28, 2026
Merged

security: replace unrestricted setattr with allowlist in Python backend#28083
titaiwangms merged 6 commits intomainfrom
fix/backend-setattr-security-allowlist

Conversation

@titaiwangms
Copy link
Copy Markdown
Contributor

@titaiwangms titaiwangms commented Apr 15, 2026

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

Both SessionOptions and RunOptions use identical validation logic with three outcomes for each kwarg key:

  • Allowlisted — Applied via setattr() (e.g. graph_optimization_level, log_severity_level)
  • Known-but-blocked (real attribute on the object, but not on allowlist) — Raises RuntimeError (e.g. optimized_model_filepath, terminate)
  • Completely unknown (not a property on the object at all) — Silently ignored for forward compatibility (e.g. nonexistent_option_xyz)

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
  • training_mode (RunOptions) — silently switches inference behavior in training builds

Tests

Added TestBackendKwargsAllowlist with 13 new test methods covering all exploit vectors (blocked attrs raise RuntimeError), safe allowlisted attrs (accepted), unknown attrs (silently ignored), and end-to-end run_model() paths for both session and run options. All 15 tests pass (13 new + 2 pre-existing in TestBackend), no regressions.

Files Changed

  • onnxruntime/python/backend/backend.py
  • onnxruntime/python/backend/backend_rep.py
  • onnxruntime/test/python/onnxruntime_test_python_backend.py
  • .agents/skills/python-kwargs-setattr-security/SKILL.md

@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
@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
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.

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
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from bf44dbe to ace56ab Compare April 21, 2026 21:37
@titaiwangms titaiwangms requested a review from Copilot April 21, 2026 22:07
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

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


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

Comment thread onnxruntime/python/backend/backend_rep.py
Comment thread onnxruntime/test/python/onnxruntime_test_python_backend.py
titaiwangms added a commit that referenced this pull request Apr 21, 2026
- Add missing silently-ignore comment in backend.py prepare()
- Improve exclusion comment style to per-attribute in backend.py
- Fix circular self-reference in run_model() docstring
- Rewrite SKILL.md rule 3 for cold readers
- Add error message assertion to test_run_model_with_blocked_run_option_raises
- Add tests for unknown kwargs through rep.run() and run_model()

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

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms requested review from Copilot and vraspar April 21, 2026 22:36
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

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


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

Comment thread onnxruntime/python/backend/backend.py Outdated
titaiwangms added a commit that referenced this pull request Apr 22, 2026
- Add missing silently-ignore comment in backend.py prepare()
- Improve exclusion comment style to per-attribute in backend.py
- Fix circular self-reference in run_model() docstring
- Rewrite SKILL.md rule 3 for cold readers
- Add error message assertion to test_run_model_with_blocked_run_option_raises
- Add tests for unknown kwargs through rep.run() and run_model()

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

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from 126e650 to 7298912 Compare April 22, 2026 18:42
@titaiwangms titaiwangms enabled auto-merge (squash) April 22, 2026 23:46
@titaiwangms titaiwangms requested a review from tianleiwu April 22, 2026 23:46
@titaiwangms
Copy link
Copy Markdown
Contributor Author

The CI failure is irrelevant.

titaiwangms added a commit that referenced this pull request Apr 23, 2026
- Add missing silently-ignore comment in backend.py prepare()
- Improve exclusion comment style to per-attribute in backend.py
- Fix circular self-reference in run_model() docstring
- Rewrite SKILL.md rule 3 for cold readers
- Add error message assertion to test_run_model_with_blocked_run_option_raises
- Add tests for unknown kwargs through rep.run() and run_model()

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

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from 7298912 to 49d368c Compare April 23, 2026 00:10
Copy link
Copy Markdown
Contributor

@tianleiwu tianleiwu left a comment

Choose a reason for hiding this comment

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

One follow-up design suggestion: the new allowlists appear complete for the direct Python properties on SessionOptions and RunOptions, but they leave no safe path for advanced runtime configuration channels that are not regular properties. CUDA graph is the clearest example: enabling it is an EP provider option (enable_cuda_graph for CUDA EP or trt_cuda_graph_enable for TensorRT EP), and per-run control uses the run config entry gpu_graph_id. If the backend API is expected to remain useful for advanced performance scenarios, consider a second explicit allowlist for safe advanced configuration channels such as curated session_config_entries, run_config_entries, and provider-specific options, instead of limiting support to plain attributes only. That would preserve the security fix without permanently excluding safe features like CUDA graph.

Copy link
Copy Markdown
Contributor Author

@titaiwangms titaiwangms left a comment

Choose a reason for hiding this comment

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

@tianleiwu Thank you for the forward-looking suggestion — this is exactly the kind of design thinking that helps the backend API stay useful beyond the immediate security fix.

I investigated this during the PR development and found that CUDA graph configuration (and advanced EP options in general) uses completely different APIs than the setattr() pattern this PR secures:

  • Provider options flow through SessionOptions.provider_options (a dict passed to add_provider_options()), not Python properties
  • Session config entries use add_session_config_entry(key, value) — a method call, not a settable attribute
  • Run config entries use add_run_config_entry(key, value) — same pattern

Since these are dict/method-based APIs rather than Python properties, the current frozenset + setattr() allowlist pattern cannot cover them. A proper implementation would need:

  1. Per-EP allowlists for provider options (each EP has different valid keys with different security implications)
  2. Structured dict parameters (not flat kwargs) to distinguish provider_options, session_config_entries, and run_config_entries
  3. Careful security auditing of each EP's option set — some EP options can trigger file I/O or network access

This is estimated at 200–400 lines of new code with its own test surface. I'd like to keep this PR focused on closing the immediate setattr() vulnerability and tackle the advanced config channels as a dedicated follow-up PR where each EP's options can be properly audited.

Would that approach work for you?

titaiwangms added a commit that referenced this pull request Apr 23, 2026
- Add missing silently-ignore comment in backend.py prepare()
- Improve exclusion comment style to per-attribute in backend.py
- Fix circular self-reference in run_model() docstring
- Rewrite SKILL.md rule 3 for cold readers
- Add error message assertion to test_run_model_with_blocked_run_option_raises
- Add tests for unknown kwargs through rep.run() and run_model()

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

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from 49d368c to 3f41410 Compare April 23, 2026 16:41
auto-merge was automatically disabled April 23, 2026 16:49

Pull request was closed

@titaiwangms titaiwangms reopened this Apr 23, 2026
@titaiwangms titaiwangms requested a review from tianleiwu April 23, 2026 16:51
titaiwangms and others added 5 commits April 23, 2026 21:17
Fixes a critical security vulnerability where user-controlled kwargs were
applied to SessionOptions and RunOptions via unrestricted setattr(), allowing
arbitrary file overwrites and denial-of-inference.

Attack vector:
  onnxruntime.backend.prepare(
      model, optimized_model_filepath='/etc/passwd'  # overwrites any file
  )

The hasattr() check was not a security guard — it passed for all exposed
pybind properties including dangerous ones.

Fix:
- Add _ALLOWED_SESSION_OPTIONS frozenset (13 safe attrs) in backend.py
- Add _ALLOWED_RUN_OPTIONS frozenset (4 safe attrs) in backend_rep.py
- Replace hasattr/setattr loops with allowlist checks; blocked-but-known
  attrs raise RuntimeError with a clear message listing allowed attrs
- run_model() passes **kwargs directly to prepare() and run() so both
  paths enforce their own allowlist (no silent drops)

Blocked dangerous attributes:
- optimized_model_filepath: triggers Model::Save(), overwrites arbitrary
  files with protobuf binary
- profile_file_prefix + enable_profiling: writes JSON to arbitrary path
- terminate (RunOptions): denies the current inference call

Tests:
- Add TestBackendKwargsAllowlist (11 tests) covering all exploit vectors,
  safe attrs accepted, unknown attrs silently ignored, run_model() paths
- Use tempfile for all temp file/dir references (no hardcoded paths)

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

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add missing silently-ignore comment in backend.py prepare()
- Improve exclusion comment style to per-attribute in backend.py
- Fix circular self-reference in run_model() docstring
- Rewrite SKILL.md rule 3 for cold readers
- Add error message assertion to test_run_model_with_blocked_run_option_raises
- Add tests for unknown kwargs through rep.run() and run_model()

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

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Agent-signed-off: Developer (1003864e) [claude-opus-4.6]
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from 3f41410 to d277b35 Compare April 23, 2026 21:17
@vraspar vraspar requested a review from Copilot April 24, 2026 19:24
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

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


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

Comment thread onnxruntime/python/backend/backend.py Outdated
Comment thread onnxruntime/python/backend/backend.py Outdated
- prepare() return type: InferenceSession -> OnnxRuntimeBackendRep
- prepare() :param model: list all 5 accepted input types
- prepare() description: references OnnxRuntimeBackendRep, not InferenceSession
- run_model() :param model: list all 5 accepted input types
- OnnxRuntimeBackendRep class docstring: clarify it wraps an InferenceSession
- OnnxRuntimeBackendRep.run(): add :param inputs:, :param kwargs:, :return: docs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@titaiwangms titaiwangms force-pushed the fix/backend-setattr-security-allowlist branch from a935997 to 342ff99 Compare April 27, 2026 20:22
Copy link
Copy Markdown
Contributor

@tianleiwu tianleiwu left a comment

Choose a reason for hiding this comment

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

Reviewed the latest head. The allowlist implementation and tests look good to me: the known file-write/inference-control attributes are blocked, unknown kwargs are still ignored where needed for the dual prepare/run forwarding path, and the earlier doc/PR-body mismatches have been addressed.

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.

4 participants