Skip to content

Harden script_reader against vulnerabilities - #546

Open
urfeex wants to merge 1 commit into
UniversalRobots:masterfrom
urfeex:harden_script_reader
Open

Harden script_reader against vulnerabilities#546
urfeex wants to merge 1 commit into
UniversalRobots:masterfrom
urfeex:harden_script_reader

Conversation

@urfeex

@urfeex urfeex commented Aug 4, 2026

Copy link
Copy Markdown
Member

This doesn't change anything in behavior, it merely enforces the behavior as it is described in the docs.

  1. Path traversal (.. and absolute-path escape) — replaceIncludes() now rejects absolute include paths outright (avoiding the path::operator/ LHS-drop trap) and requires every include's weakly_canonical-normalized target to be lexically inside
    root_dir_ (the directory of the top-level script).

  2. Unbounded / circular includes → stack overflow — added include_stack_ (set of canonical paths currently being processed) for cycle detection and include_depth_ bounded by a fixed threshold of 32. Both self-references and A→B→A cycles now throw UrException, as do chains beyond the depth cap.

  3. script_path_ state corruption from nested includes — removed the mutable script_path_ member. The public readScriptFile() is now a thin wrapper that initializes state, and a new private readScriptFileImpl() save-restores current_dir_ (and cleans up include_stack_/include_depth_) around every recursive include, even on exceptions.

  4. Malformed include regex ['|"] — changed to ['"]. {% include |target.txt| %} is no longer treated as a valid include.


Note

High Risk
Changes file-read boundaries and include recursion in script loading—a security-sensitive path where prior behavior allowed directory escape and unbounded recursion; legitimate deep include chains beyond 32 levels will now fail.

Overview
Hardens ScriptReader include processing so nested {% include %} matches documented behavior: includes stay under the top-level script directory, cannot recurse indefinitely, and sibling includes keep resolving from the parent file’s directory.

readScriptFile() now canonicalizes the entry path, sets root_dir_ from that file’s parent, and delegates to private readScriptFileImpl(). Includes resolve via current_dir_ (save/restored per file), reject absolute or rooted paths (including Windows edge cases), normalize with weakly_canonical, and throw if the target is outside root_dir_. Cycle detection (include_stack_) and a depth cap (MAX_INCLUDE_DEPTH = 32) replace the old pattern of re-entering readScriptFile() through mutable script_path_.

The include directive regex no longer treats | as a quote character (['"] instead of ['|"]). Security regression tests cover traversal, absolute paths, cycles, depth, sibling resolution, pipe quotes, and Windows-only rooted paths.

Reviewed by Cursor Bugbot for commit 7ab796b. Bugbot is set up for automated code reviews on this repo. Configure here.

1. Path traversal (.. and absolute-path escape) — `replaceIncludes()` now
   rejects absolute include paths outright (avoiding the `path::operator/`
   LHS-drop trap) and requires every include's
   `weakly_canonical`-normalized target to be lexically inside `root_dir_`
   (the directory of the top-level script).

2. Unbounded / circular includes → stack overflow — added `include_stack_`
   (set of canonical paths currently being processed) for cycle
   detection and `include_depth_` bounded by a fixed threshold of 32. Both
   self-references and A→B→A cycles now throw `UrException`, as do chains
   beyond the depth cap.

3. `script_path_` state corruption from nested includes — removed the
   mutable `script_path_` member. The public `readScriptFile()` is now a thin
   wrapper that initializes state, and a new private `readScriptFileImpl()`
   save-restores `current_dir_` (and cleans up
   `include_stack_`/`include_depth_`) around every recursive include, even
   on exceptions.

4. Malformed include regex ['|"] — changed to ['"]. {% include
   |target.txt| %} is no longer treated as a valid include.
@urfeex
urfeex requested a review from a team August 4, 2026 08:38
@urfeex urfeex added the bugfix label Aug 4, 2026
@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 81.53846% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.06%. Comparing base (3ca5f04) to head (7ab796b).

Files with missing lines Patch % Lines
src/control/script_reader.cpp 81.53% 8 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #546      +/-   ##
==========================================
- Coverage   79.17%   79.06%   -0.12%     
==========================================
  Files         115      115              
  Lines        6801     6856      +55     
  Branches     3000     3037      +37     
==========================================
+ Hits         5385     5421      +36     
- Misses       1047     1063      +16     
- Partials      369      372       +3     
Flag Coverage Δ
check_version_ur10-3.15.8 11.26% <0.00%> (-0.30%) ⬇️
check_version_ur10e-10.11.0 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur10e-5.15.2 11.07% <0.00%> (-0.05%) ⬇️
check_version_ur12e-10.12.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur12e-5.25.1 11.03% <0.00%> (-0.29%) ⬇️
check_version_ur15-10.12.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur15-5.25.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur16e-10.12.1 11.07% <0.00%> (-0.05%) ⬇️
check_version_ur16e-5.25.1 11.21% <0.00%> (+0.09%) ⬆️
check_version_ur18-10.12.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur18-5.25.1 11.03% <0.00%> (-0.46%) ⬇️
check_version_ur20-10.12.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur20-5.25.1 11.03% <0.00%> (-0.15%) ⬇️
check_version_ur3-3.14.3 11.03% <0.00%> (-0.33%) ⬇️
check_version_ur30-10.12.1 11.03% <0.00%> (-0.15%) ⬇️
check_version_ur30-5.25.1 11.03% <0.00%> (-0.29%) ⬇️
check_version_ur3e-10.11.0 11.07% <0.00%> (-0.05%) ⬇️
check_version_ur3e-5.9.4 11.07% <0.00%> (-0.29%) ⬇️
check_version_ur5-3.15.8 11.03% <0.00%> (-0.13%) ⬇️
check_version_ur5e-10.11.0 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur5e-5.12.8 11.07% <0.00%> (-0.46%) ⬇️
check_version_ur7e-10.11.0 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur7e-5.22.2 11.03% <0.00%> (-0.29%) ⬇️
check_version_ur8long-10.12.1 11.03% <0.00%> (-0.10%) ⬇️
check_version_ur8long-5.25.1 11.21% <0.00%> (+0.09%) ⬆️
python_scripts 75.90% <ø> (ø)
start_ursim 85.00% <ø> (-1.34%) ⬇️
ur20-latest 74.81% <81.53%> (+0.01%) ⬆️
ur5-3.14.3 74.63% <81.53%> (-0.05%) ⬇️
ur5e-10.11.0 69.24% <81.53%> (-0.22%) ⬇️
ur5e-10.12.0 70.38% <81.53%> (+0.06%) ⬆️
ur5e-10.7.0 68.63% <81.53%> (-0.14%) ⬇️
ur5e-5.9.4 75.31% <81.53%> (+0.11%) ⬆️
ur7e-10.13.0 70.44% <81.53%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant