Compare commits

...
Sign in to create a new pull request.

1 commit

3 changed files with 220 additions and 1 deletions

View file

@ -2,7 +2,7 @@
Name: rubygem-%{gem_name}
Version: 0.9.40
Release: 1%{?dist}
Release: 2%{?dist}
Summary: Documentation tool for consistent and usable documentation in Ruby
@ -20,6 +20,12 @@ Source0: https://rubygems.org/gems/%{gem_name}-%{version}.gem
Source1: %{gem_name}-%{version}-test-missing-files.tar.gz
# Source1 is created by $ bash %%SOURCE2 %%version
Source2: yard-create-missing-test-files.sh
# https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8
# https://github.com/lsegal/yard/commit/f78c19f0dd33a407085b4ed181bb60c0aa0078b4
Patch1: yard-0.9.44-fix-possible-path-traversal.patch
# https://github.com/lsegal/yard/security/advisories/GHSA-3jfp-46x4-xgfj
# https://github.com/sysfce2/ruby-yard/commit/bdbff11302a035ce948d8ddad804c362c31c5be1
Patch2: yard-0.9.41-fix-possible-path-traversal.patch
# The 'irb/notifier' might be required for parsing of some old Ruby code.
# https://github.com/lsegal/yard/blob/v0.9.24/lib/yard/parser/ruby/legacy/irb/slex.rb#L13
@ -58,6 +64,8 @@ Documentation for %{name}.
%prep
%setup -q -n %{gem_name}-%{version} -b1
%patch -P1 -p1
%patch -P2 -p1
mv ../%{gem_name}-%{version}.gemspec .
%build
@ -112,6 +120,11 @@ rspec -r spec_helper spec
%doc %{gem_instdir}/docs/
%changelog
* Thu May 28 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 0.9.40-2
- Backport 0.9.41 / 0.9.44 fixes for possible path traversal issues
- https://github.com/lsegal/yard/security/advisories/GHSA-3jfp-46x4-xgfj
- https://github.com/lsegal/yard/security/advisories/GHSA-pxcc-8665-phx8
* Mon Apr 13 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 0.9.40-1
- 0.9.40

View file

@ -0,0 +1,53 @@
From bdbff11302a035ce948d8ddad804c362c31c5be1 Mon Sep 17 00:00:00 2001
From: Loren Segal <lsegal@soen.ca>
Date: Thu, 16 Apr 2026 10:44:19 -0700
Subject: [PATCH] Fix possible path traversal with document_root set in yard
server
See [GHSA-xfhh-rx56-rxcr](https://github.com/lsegal/yard/security/advisories/GHSA-xfhh-rx56-rxcr)
Closes #1448
---
CHANGELOG.md | 2 ++
lib/yard/server/commands/base.rb | 2 +-
spec/server/commands/base_spec.rb | 6 ++++++
3 files changed, 9 insertions(+), 1 deletion(-)
@@ -11,6 +12,7 @@
- Add support for `rdoc-image:...` syntax in HybridMarkup (#1676)
- Add support for colon suffix code blocks in HybridMarkup (rdoc compatibility)
- Fix responsiveness and state issues with nav frame links in `yard server`
+
# [0.9.40] - April 12th, 2026
[0.9.40]: https://github.com/lsegal/yard/compare/v0.9.39...v0.9.40
diff --git a/lib/yard/server/commands/base.rb b/lib/yard/server/commands/base.rb
index ef9d83f3a..c7d207cb3 100644
--- a/lib/yard/server/commands/base.rb
+++ b/lib/yard/server/commands/base.rb
@@ -88,7 +88,7 @@ def initialize(opts = {})
# of status, headers, and body wrapped in an array.
def call(request)
self.request = request
- self.path ||= request.path_info[1..-1]
+ self.path ||= File.cleanpath(request.path_info[1..-1])
self.headers = {'Content-Type' => 'text/html'}
self.body = ''
self.status = 200
diff --git a/spec/server/commands/base_spec.rb b/spec/server/commands/base_spec.rb
index 82c9e3096..c1726845c 100644
--- a/spec/server/commands/base_spec.rb
+++ b/spec/server/commands/base_spec.rb
@@ -48,6 +48,12 @@ def run; cache 'foo' end
end
describe "#call" do
+ it "sanitizes path_info" do
+ cmd = MyProcCommand.new { self.body = path }
+ _, _, b = *cmd.call(mock_request('/../../a/b/c'))
+ expect(b).to eq ['a/b/c']
+ end
+
it "handles a NotFoundError and use message as body" do
cmd = MyProcCommand.new { raise NotFoundError, "hello world" }
s, _, b = *cmd.call(mock_request('/foo'))

View file

@ -0,0 +1,153 @@
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'