Prevent escaping the checkout root path for repositories

This commit is contained in:
Klaus Zanders
2026-03-13 10:03:57 +01:00
parent 33d208ed65
commit ec8f5f1265
4 changed files with 55 additions and 4 deletions
+8 -3
View File
@@ -201,9 +201,14 @@ module OpenProject
end
def checkout_path
Pathname(OpenProject::Configuration.scm_local_checkout_path)
.join(@identifier)
.expand_path
root = Pathname(OpenProject::Configuration.scm_local_checkout_path).expand_path.to_s
path = Pathname(root).join(@identifier).expand_path.to_s
unless path.start_with?("#{root}/")
raise ArgumentError, "Checkout path escapes the configured root directory"
end
Pathname(path)
end
def checkout_uri
@@ -111,7 +111,14 @@ module OpenProject
# Used only in the creation of a repository, at a later point
# in time, it is referred to in the root_url
def managed_repository_path
File.join(self.class.managed_root, repository_identifier)
root = File.expand_path(self.class.managed_root)
path = File.expand_path(File.join(root, repository_identifier))
unless path.start_with?("#{root}/")
raise ArgumentError, "Repository path escapes the configured managed root directory"
end
path
end
##
@@ -550,6 +550,33 @@ RSpec.describe OpenProject::SCM::Adapters::Git do
end
end
describe "#checkout_path" do
let(:repos_dir) { Dir.mktmpdir }
before do
allow(OpenProject::Configuration)
.to receive(:scm_local_checkout_path)
.and_return(repos_dir)
end
after { FileUtils.remove_entry(repos_dir) }
it "returns a path within the configured root for a normal identifier" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, "my-project")
expect(adapter.checkout_path.to_s).to eq(File.join(repos_dir, "my-project"))
end
it "raises ArgumentError when the identifier contains path traversal" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, "../../etc")
expect { adapter.checkout_path }.to raise_error(ArgumentError, /escapes the configured root/)
end
it "raises ArgumentError for an identifier that resolves to the root itself" do
adapter = described_class.new("file:///some/repo.git", nil, nil, nil, nil, ".")
expect { adapter.checkout_path }.to raise_error(ArgumentError, /escapes the configured root/)
end
end
context "with a local repository" do
it_behaves_like "git adapter specs"
end
+12
View File
@@ -125,6 +125,18 @@ RSpec.describe Repository::Git do
end
end
context "and project with traversal identifier" do
before do
instance.project = project
allow(instance).to receive(:repository_identifier).and_return("../../etc/evil")
end
it "raises ArgumentError for path traversal" do
expect { instance.managed_repository_path }
.to raise_error(ArgumentError, /escapes the configured managed root/)
end
end
context "and associated project with parent" do
let(:parent) { build(:project) }
let(:project) { build(:project, parent:) }