153 lines
5.3 KiB
Diff
153 lines
5.3 KiB
Diff
From f78c19f0dd33a407085b4ed181bb60c0aa0078b4 Mon Sep 17 00:00:00 2001
|
|
From: Loren Segal <lsegal@soen.ca>
|
|
Date: Mon, 25 May 2026 12:49:29 -0700
|
|
Subject: [PATCH] Fix possible path traversal in StaticCaching
|
|
|
|
Fixes [GHSA-pxcc-8665-phx8](https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8)
|
|
---
|
|
lib/yard/server/commands/base.rb | 12 +++------
|
|
lib/yard/server/static_caching.rb | 41 ++++++++++++++++++++++++++----
|
|
spec/server/commands/base_spec.rb | 7 +++++
|
|
spec/server/static_caching_spec.rb | 6 +++++
|
|
4 files changed, 52 insertions(+), 14 deletions(-)
|
|
|
|
diff --git a/lib/yard/server/commands/base.rb b/lib/yard/server/commands/base.rb
|
|
index c7d207cb3..9739b4c9c 100644
|
|
--- a/lib/yard/server/commands/base.rb
|
|
+++ b/lib/yard/server/commands/base.rb
|
|
@@ -1,6 +1,4 @@
|
|
# frozen_string_literal: true
|
|
-require 'fileutils'
|
|
-
|
|
module YARD
|
|
module Server
|
|
module Commands
|
|
@@ -32,6 +30,8 @@ module Commands
|
|
# @abstract
|
|
# @see #run
|
|
class Base
|
|
+ include StaticCaching
|
|
+
|
|
# @group Basic Command and Adapter Options
|
|
|
|
# @return [Hash] the options passed to the command's constructor
|
|
@@ -163,13 +163,7 @@ def render(object = nil)
|
|
# @return [String] the same cached data (for chaining)
|
|
# @see StaticCaching
|
|
def cache(data)
|
|
- if caching && adapter.document_root
|
|
- path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html')
|
|
- path = path.sub(%r{/\.html$}, '.html')
|
|
- FileUtils.mkdir_p(File.dirname(path))
|
|
- log.debug "Caching data to #{path}"
|
|
- File.open(path, 'wb') {|f| f.write(data) }
|
|
- end
|
|
+ super if caching
|
|
self.body = data
|
|
end
|
|
|
|
diff --git a/lib/yard/server/static_caching.rb b/lib/yard/server/static_caching.rb
|
|
index ca43b15ac..f91fcd825 100644
|
|
--- a/lib/yard/server/static_caching.rb
|
|
+++ b/lib/yard/server/static_caching.rb
|
|
@@ -1,4 +1,6 @@
|
|
# frozen_string_literal: true
|
|
+require 'fileutils'
|
|
+
|
|
module YARD
|
|
module Server
|
|
# Implements static caching for requests.
|
|
@@ -10,9 +12,8 @@ module StaticCaching
|
|
# implement your own +#check_static_cache+ method and mix the module into
|
|
# the Router class.
|
|
#
|
|
- # Note that caching does not occur here. This method simply checks for
|
|
- # the existence of cached data. To actually cache a response, see
|
|
- # {Commands::Base#cache}.
|
|
+ # This method checks for the existence of cached data. To actually cache
|
|
+ # a response, see {#cache}.
|
|
#
|
|
# @example Implementing In-Memory Cache Checking
|
|
# module MemoryCaching
|
|
@@ -33,14 +34,44 @@ module StaticCaching
|
|
# @see Commands::Base#cache
|
|
def check_static_cache
|
|
return nil unless adapter.document_root
|
|
- cache_path = File.join(adapter.document_root, request.path.sub(/\.html$/, '') + '.html')
|
|
- cache_path = cache_path.sub(%r{/\.html$}, '.html')
|
|
+ cache_path = cache_path(request.path)
|
|
+ return nil unless cache_path
|
|
+
|
|
if File.file?(cache_path)
|
|
log.debug "Loading cache from disk: #{cache_path}"
|
|
return [200, {'Content-Type' => 'text/html'}, [File.read_binary(cache_path)]]
|
|
end
|
|
nil
|
|
end
|
|
+
|
|
+ # Caches rendered HTML response data to disk.
|
|
+ #
|
|
+ # @param [String] data the data to cache
|
|
+ # @return [void]
|
|
+ # @since 0.9.44
|
|
+ def cache(data)
|
|
+ return unless adapter.document_root
|
|
+
|
|
+ path = cache_path(request.path_info)
|
|
+ return unless path
|
|
+
|
|
+ FileUtils.mkdir_p(File.dirname(path))
|
|
+ log.debug "Caching data to #{path}"
|
|
+ File.open(path, 'wb') {|f| f.write(data) }
|
|
+ end
|
|
+
|
|
+ private
|
|
+
|
|
+ def cache_path(request_path)
|
|
+ return nil if request_path.split(/[\/\\]/).include?('..')
|
|
+
|
|
+ path = request_path.sub(/\.html$/, '') + '.html'
|
|
+ path = path.sub(%r{\A/+}, '')
|
|
+ return nil if path =~ /\A[A-Za-z]:/
|
|
+
|
|
+ path = File.cleanpath(path)
|
|
+ File.join(adapter.document_root, path)
|
|
+ end
|
|
end
|
|
end
|
|
end
|
|
diff --git a/spec/server/commands/base_spec.rb b/spec/server/commands/base_spec.rb
|
|
index c1726845c..568dda778 100644
|
|
--- a/spec/server/commands/base_spec.rb
|
|
+++ b/spec/server/commands/base_spec.rb
|
|
@@ -36,6 +36,13 @@ def run; cache 'foo' end
|
|
@command.request.path_info = '/path/to/file.html'
|
|
@command.run
|
|
end
|
|
+
|
|
+ it "does not cache paths containing parent directory components" do
|
|
+ expect(FileUtils).not_to receive(:mkdir_p)
|
|
+ expect(File).not_to receive(:open)
|
|
+ @command.request.path_info = '/../path/to/file.html'
|
|
+ @command.run
|
|
+ end
|
|
end
|
|
|
|
describe "#redirect" do
|
|
diff --git a/spec/server/static_caching_spec.rb b/spec/server/static_caching_spec.rb
|
|
index 57960e6dd..724741e43 100644
|
|
--- a/spec/server/static_caching_spec.rb
|
|
+++ b/spec/server/static_caching_spec.rb
|
|
@@ -37,6 +37,12 @@ def request; @request ||= MockRequest.new end
|
|
expect(check_static_cache).to eq nil
|
|
end
|
|
|
|
+ it "does not read paths containing parent directory components" do
|
|
+ request.path_info = '/../secret'
|
|
+ expect(File).not_to receive(:file?)
|
|
+ expect(check_static_cache).to eq nil
|
|
+ end
|
|
+
|
|
it "adds mount point to cache location" do
|
|
request.path_info = '/hello/world.html'
|
|
request.script_name = '/mount/point'
|