From d76d7d320495734b5ea9baf5f697fb605eb7e92d Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins Date: Fri, 10 Apr 2026 18:11:52 +0100 Subject: [PATCH] Security hardening: validate release notes and workflow inputs (#7448) * [AI] Harden GitHub Actions workflows against low-severity security issues - generate-release-pr.yml: replace `eval` with an associative array for per-package version tracking. The version input was already moved to an env var in #7433, so this removes the remaining defense-in-depth concern of `eval`ing subshell output. - create-release-notes-file.js: validate the OpenAI-returned category against the known allow-list (Features, Bugfixes, Enhancements, Maintenance), validate the author against the GitHub username regex, and collapse the summary to a single line before embedding it in the markdown body. Prevents indirect prompt-injection via CodeRabbit comments from producing malformed YAML frontmatter. - generate-summary.js: stop logging the full CodeRabbit comment body to CI logs. - netlify-release.yml, i18n-string-extract-master.yml: pass secrets via `env:` blocks rather than as CLI arguments, so they do not appear in argv / process listings. https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33 * Add release notes for PR #7448 * [AI] Address review feedback on security hardening - create-release-notes-file.js: stop logging the full fileContent body. Only log the target filename plus the (already-validated) category and author metadata, so the model-generated release-note text doesn't end up in CI logs. - create-release-notes-file.js: validate summaryData.prNumber as a positive integer before using it in the file path or commit message, and switch both usages to the validated numeric value. - i18n-string-extract-master.yml: write the Weblate API key into ~/.config/weblate under a [keys] section in a new "Configure Weblate API credentials" step, then drop the per-step env blocks and the --key CLI flag from every wlc invocation so the secret is no longer visible in process listings at all. https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33 * [AI] Remove debug console.log statements for category in release notes script Remove the four "Debug - ..." console.log calls that printed the raw category env var (value/type/JSON-stringified form) plus the cleanCategory value. They were clutter in CI logs; the existing info-level "Creating release notes file: ... (category: ..., author: ...)" log already surfaces the sanitized category. https://claude.ai/code/session_012pZSkUBbabmmuaxbwysW33 --------- Co-authored-by: Claude Co-authored-by: github-actions[bot] --- .../create-release-notes-file.js | 67 +++++++++++++++---- .../generate-summary.js | 2 - .github/workflows/generate-release-pr.yml | 5 +- .../workflows/i18n-string-extract-master.yml | 16 +++-- .github/workflows/netlify-release.yml | 5 +- upcoming-release-notes/7448.md | 6 ++ 6 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 upcoming-release-notes/7448.md diff --git a/.github/actions/ai-generated-release-notes/create-release-notes-file.js b/.github/actions/ai-generated-release-notes/create-release-notes-file.js index 1709a3d912..4da0b7b862 100755 --- a/.github/actions/ai-generated-release-notes/create-release-notes-file.js +++ b/.github/actions/ai-generated-release-notes/create-release-notes-file.js @@ -16,14 +16,19 @@ if (!token || !repo || !issueNumber || !summaryDataJson || !category) { const [owner, repoName] = repo.split('/'); const octokit = new Octokit({ auth: token }); +const VALID_CATEGORIES = [ + 'Features', + 'Bugfixes', + 'Enhancements', + 'Maintenance', +]; +const GITHUB_USERNAME_RE = + /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/; + async function createReleaseNotesFile() { try { const summaryData = JSON.parse(summaryDataJson); - console.log('Debug - Category value:', category); - console.log('Debug - Category type:', typeof category); - console.log('Debug - Category JSON stringified:', JSON.stringify(category)); - if (!summaryData) { console.log('No summary data available, cannot create file'); return; @@ -34,26 +39,62 @@ async function createReleaseNotesFile() { return; } - // Create file content - ensure category is not quoted + // Normalize category - strip surrounding quotes and validate against allow-list const cleanCategory = typeof category === 'string' ? category.replace(/^["']|["']$/g, '') : category; - console.log('Debug - Clean category:', cleanCategory); + + if (!VALID_CATEGORIES.includes(cleanCategory)) { + console.log( + `Invalid category "${cleanCategory}". Must be one of: ${VALID_CATEGORIES.join(', ')}`, + ); + return; + } + + // Validate author is a plausible GitHub username + const author = String(summaryData.author || ''); + if (!GITHUB_USERNAME_RE.test(author)) { + console.log( + `Invalid author "${author}", aborting release notes creation`, + ); + return; + } + + // Normalize summary: collapse whitespace to a single line so it cannot + // introduce extra YAML frontmatter or break the markdown structure. + const cleanSummary = String(summaryData.summary || '') + .replace(/\s+/g, ' ') + .trim(); + if (!cleanSummary) { + console.log('Empty summary, aborting release notes creation'); + return; + } + + // Validate PR number - must be a positive integer. The value comes from + // the GitHub API, but we harden it because it's used to build a file path + // and a commit message. + const validatedPrNumber = Number(summaryData.prNumber); + if (!Number.isInteger(validatedPrNumber) || validatedPrNumber <= 0) { + console.log( + `Invalid PR number "${summaryData.prNumber}", aborting release notes creation`, + ); + return; + } const fileContent = `--- category: ${cleanCategory} -authors: [${summaryData.author}] +authors: [${author}] --- -${summaryData.summary} +${cleanSummary} `; - const fileName = `upcoming-release-notes/${summaryData.prNumber}.md`; + const fileName = `upcoming-release-notes/${validatedPrNumber}.md`; - console.log(`Creating release notes file: ${fileName}`); - console.log('File content:'); - console.log(fileContent); + console.log( + `Creating release notes file: ${fileName} (category: ${cleanCategory}, author: ${author})`, + ); // Get PR info const { data: pr } = await octokit.rest.pulls.get({ @@ -75,7 +116,7 @@ ${summaryData.summary} owner: headOwner, repo: headRepo, path: fileName, - message: `Add release notes for PR #${summaryData.prNumber}`, + message: `Add release notes for PR #${validatedPrNumber}`, content: Buffer.from(fileContent).toString('base64'), branch: prBranch, committer: { diff --git a/.github/actions/ai-generated-release-notes/generate-summary.js b/.github/actions/ai-generated-release-notes/generate-summary.js index 244d0b2f45..bc362fed8e 100755 --- a/.github/actions/ai-generated-release-notes/generate-summary.js +++ b/.github/actions/ai-generated-release-notes/generate-summary.js @@ -25,8 +25,6 @@ try { process.exit(0); } - console.log('CodeRabbit comment body:', commentBody); - const data = JSON.stringify({ model: 'gpt-4o-mini', messages: [ diff --git a/.github/workflows/generate-release-pr.yml b/.github/workflows/generate-release-pr.yml index df04aa8f95..3764616f77 100644 --- a/.github/workflows/generate-release-pr.yml +++ b/.github/workflows/generate-release-pr.yml @@ -38,6 +38,7 @@ jobs: [cli]="cli" [core]="loot-core" ) + declare -A new_versions for key in "${!packages[@]}"; do pkg="${packages[$key]}" @@ -54,10 +55,10 @@ jobs: --update) fi - eval "NEW_${key^^}_VERSION=\"$version\"" + new_versions[$key]="$version" done - echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT" + echo "version=${new_versions[web]}" >> "$GITHUB_OUTPUT" - name: Create PR uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: diff --git a/.github/workflows/i18n-string-extract-master.yml b/.github/workflows/i18n-string-extract-master.yml index bcbcef9023..60c61771b1 100644 --- a/.github/workflows/i18n-string-extract-master.yml +++ b/.github/workflows/i18n-string-extract-master.yml @@ -27,12 +27,23 @@ jobs: - name: Configure i18n client run: | pip install wlc + - name: Configure Weblate API credentials + env: + WEBLATE_API_KEY: ${{ secrets.WEBLATE_API_KEY_CI_STRINGS }} + run: | + # Write the API key to wlc's config file instead of passing it on + # the command line, so the secret doesn't appear in process listings. + mkdir -p "$HOME/.config" + umask 077 + cat > "$HOME/.config/weblate" <