mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-06 15:12:35 -05:00
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 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d8317c44b7
commit
d76d7d3204
@@ -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: {
|
||||
|
||||
@@ -25,8 +25,6 @@ try {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('CodeRabbit comment body:', commentBody);
|
||||
|
||||
const data = JSON.stringify({
|
||||
model: 'gpt-4o-mini',
|
||||
messages: [
|
||||
|
||||
5
.github/workflows/generate-release-pr.yml
vendored
5
.github/workflows/generate-release-pr.yml
vendored
@@ -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:
|
||||
|
||||
16
.github/workflows/i18n-string-extract-master.yml
vendored
16
.github/workflows/i18n-string-extract-master.yml
vendored
@@ -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" <<EOF
|
||||
[keys]
|
||||
https://hosted.weblate.org/api/ = ${WEBLATE_API_KEY}
|
||||
EOF
|
||||
|
||||
- name: Lock translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
lock \
|
||||
actualbudget/actual
|
||||
|
||||
@@ -40,7 +51,6 @@ jobs:
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
push \
|
||||
actualbudget/actual
|
||||
- name: Check out updated translations
|
||||
@@ -73,7 +83,6 @@ jobs:
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
pull \
|
||||
actualbudget/actual
|
||||
|
||||
@@ -82,6 +91,5 @@ jobs:
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
unlock \
|
||||
actualbudget/actual
|
||||
|
||||
5
.github/workflows/netlify-release.yml
vendored
5
.github/workflows/netlify-release.yml
vendored
@@ -34,10 +34,11 @@ jobs:
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify_deploy
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }}
|
||||
run: |
|
||||
netlify deploy \
|
||||
--dir packages/desktop-client/build \
|
||||
--site ${{ secrets.NETLIFY_SITE_ID }} \
|
||||
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
|
||||
--filter @actual-app/web \
|
||||
--prod
|
||||
|
||||
6
upcoming-release-notes/7448.md
Normal file
6
upcoming-release-notes/7448.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Add input validation for release notes and refactor credential handling in GitHub workflows.
|
||||
Reference in New Issue
Block a user