From 77cbd38fa1e68dcd66815598757bcd5f0754cce7 Mon Sep 17 00:00:00 2001 From: weijian Date: Mon, 7 Sep 2026 15:39:39 +0800 Subject: [PATCH] fix: normalize upstream sync root paths --- .github/workflows/skill-gate.yml | 3 +++ README.md | 1 + scripts/sync_upstreams.rb | 26 ++++++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/.github/workflows/skill-gate.yml b/.github/workflows/skill-gate.yml index 3aac5df..5407644 100644 --- a/.github/workflows/skill-gate.yml +++ b/.github/workflows/skill-gate.yml @@ -26,5 +26,8 @@ jobs: - name: Run parser regression test run: ruby scripts/validate_skills.rb --self-test + - name: Run upstream sync regression test + run: ruby scripts/sync_upstreams.rb --self-test + - name: Validate skill tree run: ruby scripts/validate_skills.rb diff --git a/README.md b/README.md index e44c7dc..ddf54ec 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ author: anthropics ```shell ruby scripts/validate_skills.rb --self-test +ruby scripts/sync_upstreams.rb --self-test ruby scripts/validate_skills.rb ruby scripts/sync_upstreams.rb ``` diff --git a/scripts/sync_upstreams.rb b/scripts/sync_upstreams.rb index 9d42e85..d67df3b 100644 --- a/scripts/sync_upstreams.rb +++ b/scripts/sync_upstreams.rb @@ -81,6 +81,10 @@ def binary_bytes?(bytes) end def safe_join(root, relative) + # `upstreamPath: .` is a valid root. Normalize it before comparing the + # expanded child path; otherwise a root ending in `/.` makes every file + # look like it escaped the repository (for example `.gitattributes`). + root = File.expand_path(root) path = File.expand_path(relative, root) unless path == root || path.start_with?("#{root}#{File::SEPARATOR}") raise SyncError, "refusing path outside repository: #{relative.inspect}" @@ -136,6 +140,28 @@ def write_github_outputs(changed, conflicts) end end +def run_self_test + Dir.mktmpdir("easycode-skill-sync-self-test-") do |root| + root_with_dot = File.join(root, ".") + expected = File.join(root, ".gitattributes") + actual = safe_join(root_with_dot, ".gitattributes") + raise "root-relative path was not normalized" unless actual == expected + + begin + safe_join(root_with_dot, "../outside") + raise "path traversal was not rejected" + rescue SyncError + # Expected: safe_join must reject paths outside the repository root. + end + end + puts "[self-test] safe_join root normalization and traversal guard passed" +end + +if ARGV.delete("--self-test") + run_self_test + exit 0 +end + targets = [] Dir[File.join(SKILLS_ROOT, "*", "SKILL.md")].sort.each do |skill_path| metadata = parse_frontmatter(skill_path)