diff --git a/steps/generate-release-summary/generateReleaseSummary.js b/steps/generate-release-summary/generateReleaseSummary.js index ecd3bba..ef2d587 100644 --- a/steps/generate-release-summary/generateReleaseSummary.js +++ b/steps/generate-release-summary/generateReleaseSummary.js @@ -25,6 +25,11 @@ const runGit = (args, options = {}) => { const GIT_REF_RE = /^[A-Za-z0-9@._/-]{1,255}$/; const isSafeGitRef = (ref) => typeof ref === 'string' && GIT_REF_RE.test(ref); +// A tag glob is a ref with glob syntax: '*', '?' and bracket classes like '[0-9]'. Each glob +// token stands for ref characters, so it is replaced by one before the ref check. +const isSafeTagGlob = (glob) => + typeof glob === 'string' && isSafeGitRef(glob.replace(/\[[^\]]*\]|[*?]/g, 'x')); + // owner/repo used to linkify PR numbers. Validate strictly; fall back to a non-linking // placeholder if the env value is malformed so we never build a broken/injected URL. const resolveRepo = () => { @@ -326,7 +331,7 @@ const generateSummary = async ({ core }) => { const repo = resolveRepo(); let baseRef = process.env.BASE_REF; - if (!isSafeGitRef(tagGlob.replace(/\*/g, 'x'))) { + if (!isSafeTagGlob(tagGlob)) { throw new Error(`Refusing unsafe tag glob: ${tagGlob}`); } @@ -422,6 +427,7 @@ module.exports = { commitMatchesPathFilter, detectLatestTag, generateSummary, + isSafeTagGlob, linearAuthorization, readPathFilter, resolveCommitTitle, diff --git a/steps/generate-release-summary/generateReleaseSummary.test.js b/steps/generate-release-summary/generateReleaseSummary.test.js index 628879d..32bbc6f 100644 --- a/steps/generate-release-summary/generateReleaseSummary.test.js +++ b/steps/generate-release-summary/generateReleaseSummary.test.js @@ -8,6 +8,7 @@ const { collectScopedCommits, detectLatestTag, generateSummary, + isSafeTagGlob, linearAuthorization, readPathFilter, resolveCommitTitle, @@ -176,6 +177,16 @@ test('monorepo: keeps release-window commits and excludes everything the package } }); +test('accepts the glob syntax the release workflows build, refuses shell metacharacters', () => { + assert.equal(isSafeTagGlob('v*'), true); + assert.equal(isSafeTagGlob('v[0-9]*'), true); + assert.equal(isSafeTagGlob('@aragon/app@[0-9]*'), true); + assert.equal(isSafeTagGlob('v?.?.?'), true); + assert.equal(isSafeTagGlob('v*;rm -rf .'), false); + assert.equal(isSafeTagGlob('v$(id)*'), false); + assert.equal(isSafeTagGlob(''), false); +}); + test('single-package: v* boundary, no path filter, drops both release-commit styles', () => { const repository = createRepository();