From dab09ea6c924fb443659c057a25ed4e86e7f2e7f Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 20:39:34 +1200 Subject: [PATCH 1/2] Constrain content paths to the content root. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- lib/utopia/content/node.rb | 10 ++++------ releases.md | 1 + test/utopia/content/node.rb | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/utopia/content/node.rb b/lib/utopia/content/node.rb index 593e020a..3f5ac450 100644 --- a/lib/utopia/content/node.rb +++ b/lib/utopia/content/node.rb @@ -51,14 +51,12 @@ def lookup_node(path) def local_path(path = ".", base = nil) path = Path[path] - root = Pathname.new(@controller.root) - - if path.absolute? - return root.join(*path.components) - else + if path.relative? base ||= uri_path.dirname - return root.join(*(base + path).components) + path = base + path end + + return Pathname.new(path.to_url_path.local_path(@controller.root)) end # Resolve a path relative to this node's containing URI path. diff --git a/releases.md b/releases.md index 38470144..3c4e24a6 100644 --- a/releases.md +++ b/releases.md @@ -5,6 +5,7 @@ - **Breaking** Remove support for JavaScript packages installed in `lib/components`; use `node_modules` instead. - **Breaking** Expose {ruby Utopia::Content::Middleware#links} as the content link resolver rather than an indexed lookup method. - **Security** Authenticate encrypted session cookies using AES-256-GCM. Existing session cookies are invalidated. + - **Security** Constrain content node local paths to the configured content root. - **Security** Redact sensitive exception report fields and make bounded request body attachments opt-in. - Return `416 Range Not Satisfiable` for unsatisfiable static file byte ranges. diff --git a/test/utopia/content/node.rb b/test/utopia/content/node.rb index f2800026..0e58a8bb 100644 --- a/test/utopia/content/node.rb +++ b/test/utopia/content/node.rb @@ -84,6 +84,20 @@ expect(node.local_path("/shared/preview.jpg")).to be == (base + "shared/preview.jpg") end + + it "rejects absolute paths which escape the content root" do + node = content.lookup_node(Utopia::Path["/ordered/index"]) + + expect do + node.local_path("/../../outside") + end.to raise_exception(ArgumentError, message: be =~ /escapes the specified root/) + end + + it "contains relative paths within the content root" do + node = content.lookup_node(Utopia::Path["/ordered/index"]) + + expect(node.local_path("../../../outside")).to be == (base + "outside") + end end with "#relative_path" do From 9345f01eb847b1a4650f598b113f7bbeb1caf7a4 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 17 Aug 2026 20:42:38 +1200 Subject: [PATCH 2/2] Reframe content path containment. Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946 --- releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases.md b/releases.md index 3c4e24a6..dbf7cb73 100644 --- a/releases.md +++ b/releases.md @@ -5,7 +5,7 @@ - **Breaking** Remove support for JavaScript packages installed in `lib/components`; use `node_modules` instead. - **Breaking** Expose {ruby Utopia::Content::Middleware#links} as the content link resolver rather than an indexed lookup method. - **Security** Authenticate encrypted session cookies using AES-256-GCM. Existing session cookies are invalidated. - - **Security** Constrain content node local paths to the configured content root. + - Constrain content node local paths to the configured content root. - **Security** Redact sensitive exception report fields and make bounded request body attachments opt-in. - Return `416 Range Not Satisfiable` for unsatisfiable static file byte ranges.