Skip to content

fix(rgp): reject traversal components and drop raw-path fallback#33

Open
bl4ckr0ss3 wants to merge 2 commits into
orhun:mainfrom
bl4ckr0ss3:fix/rgp-path-traversal
Open

fix(rgp): reject traversal components and drop raw-path fallback#33
bl4ckr0ss3 wants to merge 2 commits into
orhun:mainfrom
bl4ckr0ss3:fix/rgp-path-traversal

Conversation

@bl4ckr0ss3
Copy link
Copy Markdown

closes #32

what

two changes in src/model.rs:

  1. drop the .or_else(|_| load_obj_meshes_from_path(path)) fallback in load_object_source. it retried with the raw, unsanitized attacker path on any error from the sanitized lookup, which defeated object_asset_path for any input the sanitizer rewrote into a non-existent location.
  2. reject ParentDir / RootDir / CurDir components in both branches of object_asset_path (the assets-anchor branch and the no-anchor fallback). legitimate inputs are unchanged.

verified

added five unit tests in src/model.rs::tests covering:

  • relative ../ traversal -> rejected
  • traversal after an assets/ anchor -> rejected
  • absolute path -> rejected
  • bare filename -> still resolves to objects/<file>
  • assets/objects/<file> -> still resolves to objects/<file>

also ran a standalone repro (no bevy/tobj, plain rustc) that mirrors the full load_object_source resolution chain against both attack and legitimate inputs - all attacks now rejected, all legitimate paths unchanged. output:

[ok]   "../../tmp/x.obj" -> rejected (asset path contains traversal components: ../../tmp/x.obj)
[ok]   "../../../../../../../tmp/ratty-victim.obj" -> rejected (...)
[ok]   "assets/../etc/passwd.obj" -> rejected (...)
[ok]   "/etc/passwd.obj" -> rejected (absolute path is outside the asset root)
[ok]   "CairoSpinyMouse.obj" -> "objects/CairoSpinyMouse.obj"
[ok]   "assets/objects/CairoSpinyMouse.obj" -> "objects/CairoSpinyMouse.obj"
[ok]   "assets/foo.obj" -> "foo.obj"
[ok]   "subdir/foo.obj" -> "subdir/foo.obj"

standalone repro source: https://gist.github.com/bl4ckr0ss3/35afe3ed0a29a42345f1e8f2a67be371 (i'll push the fixed version there too if useful).

not in this PR

there are a few smaller adjacent things i noticed while auditing (APC parser memory DoS via incomplete sequences in inline.rs, kitty PNG decode without size limits in kitty.rs::finalize, kitty multipart byte buffer unbounded growth). i'll open separate issues for those, keeping this PR focused on the traversal fix.

The RGP `r` (register) sequence accepts a filesystem path from the
terminal stream. `object_asset_path` was meant to sanitize it, but
`load_object_source` also retried with the raw original `path` on the
first error, so the sanitization was bypassed for any input the
sanitizer rewrote into a non-existent location. The sanitizer itself
also did not filter `..` components in the no-`assets`-anchor branch.

Result: any process that can write to the terminal (`cat poisoned.log`,
SSH banner, etc.) could make ratty load an arbitrary .obj/.glb from the
user's filesystem.

Fix:

1. Drop the `.or_else(|_| load_obj_meshes_from_path(path))` fallback in
   `load_object_source` so only the sanitized candidate is ever opened.
2. Reject any `ParentDir` / `RootDir` / `CurDir` component in
   `object_asset_path`, in both the `assets`-anchor and no-anchor
   branches. Legitimate inputs (bare filenames, `assets/...` paths)
   keep working unchanged.

Closes orhun#32.
@orhun
Copy link
Copy Markdown
Owner

orhun commented May 12, 2026

Thanks for the detailed issue and the PR!

This looks good mostly, but I wasn't able to run the widget examples. Especially document and draw:

2026-05-12T15:56:28.693336Z  WARN ratty::inline: failed to load RGP object 100: failed to read assets/sprite_12_offset_32218.obj: open file failed
2026-05-12T15:56:28.693658Z  WARN ratty::inline: failed to load RGP object 101: failed to read assets/black.obj: open file failed
2026-05-12T15:56:30.151345Z  WARN ratty::inline: failed to load RGP object 102: failed to read assets/bomber.obj: open file failed
2026-05-12T15:56:30.151383Z  WARN ratty::inline: failed to load RGP object 103: failed to read assets/battle.obj: open file failed
2026-05-12T15:56:30.334139Z  WARN ratty::inline: failed to load RGP object 104: failed to read assets/sprite_18_offset_48048.obj: open file failed
2026-05-12T15:56:31.465527Z  WARN ratty::inline: failed to load RGP object 102: failed to read assets/bomber.obj: open file failed
2026-05-12T15:56:31.465582Z  WARN ratty::inline: failed to load RGP object 103: failed to read assets/battle.obj: open file failed
2026-05-12T15:56:31.648197Z  WARN ratty::inline: failed to load RGP object 101: failed to read assets/black.obj: open file failed
2026-05-12T15:56:31.782133Z  WARN ratty::inline: failed to load RGP object 100: failed to read assets/sprite_12_offset_32218.obj:

Can you adjust the widget examples based on the new asset loading changes as well?

The widget examples were sending absolute paths (workspace-rooted
`widget/assets/<obj>` and `target/live_draw.obj`) and relying on the
`.or_else(|_| load_obj_meshes_from_path(path))` fallback in
`load_object_source` to open them directly. That fallback was the
traversal primitive removed in the previous commit, so the examples
now fail to load any object.

Move the widget OBJ assets into the project's asset root
(`assets/widget/<name>`) and have each example send the sanitizer-
friendly relative path `widget/<name>`. Ratty resolves that to
`assets/widget/<name>` via the same logic that already handles bare
asset filenames.

`draw` writes its live-drawn preview to
`assets/widget/.live_draw.obj` (gitignored) and sends
`widget/.live_draw.obj`. `document` discovers and sends OBJ assets
from the new location. `TempleOS.jpg` stays under `widget/assets/`
because it is loaded directly by the widget process, not by Ratty.

Per @orhun feedback on orhun#33.
@bl4ckr0ss3
Copy link
Copy Markdown
Author

thanks for testing! pushed a follow-up commit that updates the widget examples to work with the new restricted loader.

changes:

  • moved widget/assets/*.obj -> assets/widget/*.obj so they live under the project asset root
  • document example sends widget/<name> (relative, sanitizer-friendly) instead of the previous absolute workspace path. ratty resolves that to assets/widget/<name> via the same logic that handles bare filenames.
  • draw writes its live preview to assets/widget/.live_draw.obj (gitignored) and sends widget/.live_draw.obj
  • TempleOS.jpg stays at widget/assets/TempleOS.jpg since the widget reads it directly via image::ImageReader::open (ratty doesn't see it)

verified cargo build --examples in widget/ is clean. the unit tests in model.rs::tests still pass against the original sanitizer changes.

lmk if you want big_rat updated too for consistency (it sends an absolute path to assets/objects/SpinyMouse.glb which currently works because the sanitizer strips up to the assets component and that file already lives at assets/objects/), or if it's fine as-is.

@orhun
Copy link
Copy Markdown
Owner

orhun commented May 12, 2026

Moving widget assets to the root assets folder is not a good solution. It means that we will start embedding those assets into the Ratty binary which is unnecessary.

@bl4ckr0ss3
Copy link
Copy Markdown
Author

rust_embed only globs assets/objects/ (verified in src/model.rs:15-17), so I don't think assets/widget/ would end up at blob, but I understand the concern so I'll look into the other way but first I need to cook food for the dinner >..<

@orhun
Copy link
Copy Markdown
Owner

orhun commented May 12, 2026

oh got it, you're right!

@orhun
Copy link
Copy Markdown
Owner

orhun commented May 12, 2026

but then those assets won't be a part of the widget's crates.io release (which only includes the widget directory) which is still problematic :/

I think we should have those widgets assets in the widget folder respectively

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.

RGP path traversal: terminal escape can load arbitrary files via cursor/object asset loader

2 participants