Skip to content

fix(eval): Use safe dumper and yaml load - #2082

Merged
tgasser-nv merged 5 commits into
developfrom
fix/safe-yaml
Jun 26, 2026
Merged

fix(eval): Use safe dumper and yaml load#2082
tgasser-nv merged 5 commits into
developfrom
fix/safe-yaml

Conversation

@tgasser-nv

@tgasser-nv tgasser-nv commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

Description

The nemoguardrails eval tool deserializes and serializes data using the load_dict_from_file() and save_dict_to_file() functions respectively. Prior to this PR, unsafe libraries were used to save and load files. This PR changes them to use the safe equivalent (which can't deserialize and execute arbitrary python code), and adds unit-tests to make sure this isn't possible.

Changes made are:

  • Deserialization: yaml.load() -> yaml.safe_load()
  • Serialization: CDumper/Dumper -> CSafeDumper/SafeDumper.

No functional changes were made.

Related Issue(s)

Verification

Pre-commit

$ poetry run pre-commit run --all-files
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff (legacy alias)......................................................Passed
ruff format..............................................................Passed
Insert license in comments...............................................Passed
pyright..................................................................Passed

Unit-test

$ make test
=================================================== test session starts ===================================================platform darwin -- Python 3.13.2, pytest-8.4.2, pluggy-1.6.0
rootdir: /Users/tgasser/projects/nemo_guardrails_worktree/fix/safe-yaml
configfile: pytest.ini
testpaths: tests, benchmark/tests
plugins: anyio-4.12.1, inline-snapshot-0.33.0, recording-0.13.4, langsmith-0.7.12, xdist-3.8.0, httpx-0.35.0, asyncio-0.26.0, profiling-1.8.1, cov-7.0.0
asyncio: mode=Mode.STRICT, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
10 workers [5207 items]
...s............................................................................................................... [  2%]
....................................................................................................s.sss.s..s..s.. [  4%]
.s..s......ss............................................................s......................................... [  6%]
................................................................................................................... [  8%]
................................................................s.................................................. [ 11%]
........................................................................................................s.......... [ 13%]
................................................................................................................... [ 15%]
..........................................................................................................s........ [ 17%]
................................................................................................................... [ 19%]
................................................................................................................... [ 22%]
................................................................................................................... [ 24%]
................................................................................................................... [ 26%]
................................................................................................................... [ 28%]
................................................................................................................... [ 30%]
................................................................................................................... [ 33%]
.......s...........................................s.s.....................s....................................... [ 35%]
.....ssss..s.......s..s........................ss.ss..ssss......................s...............................s.. [ 37%]
................................................................................................................... [ 39%]
................................................................................................................... [ 41%]
.........................................................................................................ssssssss.. [ 44%]
.sssss................................................................................................ss........... [ 46%]
................................................................................................................... [ 48%]
........s.......................................................................................................... [ 50%]
................................................................................................................... [ 53%]
........................................ss.sss.......sssssss.sssssssss.ss.......................................... [ 55%]
................................................................................................................... [ 57%]
..............s..............................................................s.s.s.sssss......ssssss...ss.......... [ 59%]
s........................................ssssssss.................................................................. [ 61%]
................................................................................................................... [ 64%]
................................................................................................................... [ 66%]
................................................................................................................... [ 68%]
................................................................................................................... [ 70%]
......................sssss...........................................................s....sssssss................. [ 72%]
................................................................................................................... [ 75%]
................................................................................................................... [ 77%]
...........................................................s............ss......................................... [ 79%]
.....................................ss...ss............................s....ss...............................s.... [ 81%]
......................................sssssssssssss............ss.................................................. [ 83%]
...................................................................................s............................... [ 86%]
....................s...........s......ss............................................s............................. [ 88%]
.........ss.ssss.................................sss............................................................... [ 90%]
.................................................................................sssssssss.ssssss.ssss............. [ 92%]
......................................................................s............................................ [ 94%]
............................................................................s...................................... [ 97%]
.................................................s................................................................. [ 99%]
................................                                                                                    [100%]

═════════════════════════════════════════════════════ inline-snapshot ═════════════════════════════════════════════════════
INFO: inline-snapshot was disabled because you used xdist. This means that tests with snapshots will continue to run, but
snapshot(x) will only return x and inline-snapshot will not be able to fix snapshots or generate reports.


=========================================== 5029 passed, 178 skipped in 32.24s ============================================

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: ___).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • Bug Fixes
    • Improved YAML file handling to use safer parsing and writing defaults.
    • YAML inputs with unsafe Python object tags are now rejected, preventing unintended side effects.
    • Ordinary YAML and JSON files continue to load normally, and basic data now round-trips correctly when saved and reloaded.
    • If the faster YAML dumper isn’t available, the app now falls back to a safe alternative automatically.

@github-actions github-actions Bot added status: needs triage New issues that have not yet been reviewed or categorized. size: M labels Jun 26, 2026
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tgasser-nv tgasser-nv added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Jun 26, 2026
@tgasser-nv tgasser-nv self-assigned this Jun 26, 2026
@tgasser-nv
tgasser-nv marked this pull request as ready for review June 26, 2026 16:49
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a YAML deserialization and serialization security issue in the eval tool by replacing unsafe PyYAML APIs with their safe equivalents, preventing arbitrary Python object instantiation from attacker-controlled YAML files.

  • Load path (load_dict_from_file): replaces yaml.load(f, Loader=CLoader/Loader) with yaml.safe_load(f), eliminating the ability for !!python/object/apply tags to execute arbitrary callables.
  • Dump path (save_dict_to_file): replaces CDumper/Dumper with CSafeDumper/SafeDumper, preventing serialization of arbitrary Python objects into YAML that would be rejected by the safe loader anyway.
  • A new test suite (tests/eval/test_utils_safe_yaml.py) covers rejection of malicious tags, benign YAML/JSON round-trips, and the fallback from CSafeDumper to SafeDumper when libyaml is unavailable.

Confidence Score: 5/5

Safe to merge — a targeted security hardening with no functional changes and good test coverage.

Both the load and dump paths are switched to safe equivalents in a minimal, correct way. The unused CLoader/Loader import is cleanly removed. The new tests confirm that malicious !!python/object/apply tags are rejected at load time, arbitrary Python objects are refused at dump time, benign data round-trips correctly, and the CSafeDumper → SafeDumper fallback works as intended. No pre-existing callers are broken because the safe APIs accept the same input shapes as the unsafe ones for all ordinary YAML/JSON data.

No files require special attention.

Important Files Changed

Filename Overview
nemoguardrails/eval/utils.py Replaced unsafe yaml.load/CDumper with yaml.safe_load/CSafeDumper (falling back to SafeDumper); the unused CLoader/Loader import is also removed. The change is minimal and correct.
tests/eval/test_utils_safe_yaml.py New test file covering malicious-tag rejection (load and directory walk), benign YAML/JSON loading, safe-dumper fallback, serialization refusal for arbitrary objects, and a full round-trip — thorough coverage of the security fix.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Caller] --> B{load_dict_from_file}
    B -->|.yaml / .yml| C[yaml.safe_load]
    B -->|.json| D[json.load]
    C -->|contains !!python/... tag| E[ConstructorError raised]
    C -->|benign YAML| F[Return dict]
    D --> F

    G[Caller] --> H{save_dict_to_file}
    H -->|.yaml / .yml| I{CSafeDumper available?}
    H -->|.json| J[json.dumps]
    I -->|Yes - libyaml present| K[yaml.dump with CSafeDumper]
    I -->|No - pure Python| L[yaml.dump with SafeDumper]
    K -->|contains arbitrary Python object| M[RepresenterError raised]
    K -->|primitive data| N[Write YAML file]
    L -->|primitive data| N
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Caller] --> B{load_dict_from_file}
    B -->|.yaml / .yml| C[yaml.safe_load]
    B -->|.json| D[json.load]
    C -->|contains !!python/... tag| E[ConstructorError raised]
    C -->|benign YAML| F[Return dict]
    D --> F

    G[Caller] --> H{save_dict_to_file}
    H -->|.yaml / .yml| I{CSafeDumper available?}
    H -->|.json| J[json.dumps]
    I -->|Yes - libyaml present| K[yaml.dump with CSafeDumper]
    I -->|No - pure Python| L[yaml.dump with SafeDumper]
    K -->|contains arbitrary Python object| M[RepresenterError raised]
    K -->|primitive data| N[Write YAML file]
    L -->|primitive data| N
Loading

Reviews (3): Last reviewed commit: "Add escaping to YAML" | Re-trigger Greptile

Comment thread nemoguardrails/eval/utils.py Outdated
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

YAML loading now uses safe_load, YAML output prefers CSafeDumper with a SafeDumper fallback, and new tests cover unsafe-tag rejection, JSON parsing, dumper fallback, and save/load round-trips.

Changes

Safe YAML utilities

Layer / File(s) Summary
Safe loader and dumper selection
nemoguardrails/eval/utils.py, tests/eval/test_utils_safe_yaml.py
load_dict_from_file uses yaml.safe_load for YAML files, and the module now prefers CSafeDumper with a SafeDumper fallback; the test module verifies the fallback path.
Load-path safety tests
tests/eval/test_utils_safe_yaml.py
The test module sets up helpers and checks that unsafe YAML tags raise yaml.constructor.ConstructorError, benign YAML loads unchanged, and .json inputs use the JSON branch.
Save-path safety tests
tests/eval/test_utils_safe_yaml.py
The test module checks that save_dict_to_file rejects custom objects with yaml.representer.RepresenterError and round-trips primitive data through save and load.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: switching eval YAML handling to safe load/dump APIs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Results For Major Changes ✅ Passed PR description includes verification with pre-commit and full test-suite results, satisfying the testing-information requirement.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/safe-yaml

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/eval/test_utils_safe_yaml.py`:
- Line 42: The YAML fixtures in the safe YAML tests interpolate a raw filesystem
path into a double-quoted YAML string, which breaks parsing on Windows because
backslashes are treated as escapes. Update the path handling in the affected
test cases in test_utils_safe_yaml.py by escaping the path before embedding it
in the YAML content, so the tests reach the expected ConstructorError instead of
failing during YAML parsing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1afddb2e-d0e1-4d7d-b726-66e563a0d92f

📥 Commits

Reviewing files that changed from the base of the PR and between 62f5a95 and 58fe7a1.

📒 Files selected for processing (2)
  • nemoguardrails/eval/utils.py
  • tests/eval/test_utils_safe_yaml.py

Comment thread tests/eval/test_utils_safe_yaml.py Outdated
@tgasser-nv
tgasser-nv merged commit 289c0f3 into develop Jun 26, 2026
36 checks passed
@tgasser-nv
tgasser-nv deleted the fix/safe-yaml branch June 26, 2026 18:28
RobGeada pushed a commit to RobGeada/NeMo-Guardrails that referenced this pull request Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: M status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants