From f0fb160dbfb78ed3df9c19662b449e740fbb531c Mon Sep 17 00:00:00 2001 From: Sean Tang <171081544+seant-aws@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:21:35 +0000 Subject: [PATCH] docs(expr): warn that is_relative_to is a lexical prefix test, not containment is_relative_to treats '..' as an ordinary component, matching Python's PurePosixPath.is_relative_to(). Document that it should not be used alone to confine untrusted paths, and provide the safe idiom. Signed-off-by: Sean Tang <171081544+seant-aws@users.noreply.github.com> --- specs/expr/path-mapping.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/specs/expr/path-mapping.md b/specs/expr/path-mapping.md index 1313d148..232fb637 100644 --- a/specs/expr/path-mapping.md +++ b/specs/expr/path-mapping.md @@ -150,9 +150,18 @@ Path-related operations in the expression language: | `with_number(n)` | `(path, int) -> path` / `(string, int) -> path` | Append frame number | | `as_posix()` | `(path) -> string` | Convert to POSIX string | | `is_absolute()` | `(path) -> bool` | Check if path is absolute | -| `is_relative_to(other)` | `(path, path) -> bool` | Check prefix relationship | +| `is_relative_to(other)` | `(path, path) -> bool` | Lexical prefix test (see note below) | | `relative_to(other)` | `(path, path) -> path` | Compute relative path | +> **`is_relative_to` is a lexical prefix test, not a containment +> check.** `..` segments are ordinary components (see +> [`path-parse.md`](path-parse.md#normalization)), so +> `path("/allowed/../etc/passwd").is_relative_to(path("/allowed"))` +> returns `true`. This matches `PurePosixPath.is_relative_to()`. +> To reject traversal, combine with a `..` check: +> `p.is_relative_to(base) and not ("..") in p.parts` +> (note: `.parts` is a property, not a function). + > **URI paths in path methods.** The path properties and the > `with_*` methods all detect URI inputs (`uri_path::is_uri`) and > route to `uri_path::*` so the result preserves URI grammar —