Skip to content

fix(release-tools): treat proposal titles as untrusted in add-release-to-changelog.sh - #12083

Open
pietrodimarco-dfinity wants to merge 1 commit into
masterfrom
claude/security-finding-fix-bfc1e6
Open

pietrodimarco-dfinity wants to merge 1 commit into
masterfrom
claude/security-finding-fix-bfc1e6

Conversation

@pietrodimarco-dfinity

Copy link
Copy Markdown
Contributor

rs/nervous_system/tools/release/add-release-to-changelog.sh (run by release engineers per changelog_process.md, and by release-runscript for each proposal ID) derives a canister name and a commit ID from an NNS proposal's title. It uses them to pick which CHANGELOG.md / unreleased_changelog.md to rewrite. The title is free text, but the script handled it as if it were trusted. This PR fixes that.

Changes

  • Canister-name lookup no longer uses indirect expansion (lib/functions.sh). get_nns_canister_code_location and get_sns_canister_code_location used to build a variable name from the canister name and read it with ${!name}. Bash evaluates array subscripts in such names, including command substitutions. Both functions now use a case over the known canister names and return an error for any other name. Output for every known name (hyphenated or underscored) is byte-identical to before. Unknown names already failed before, with "unbound variable" under set -u.
  • Strict commit check (add-release-to-changelog.sh). The commit ID from the title must be 7–40 lowercase hex characters. It is compared with HEAD as a quoted literal prefix. Before, the pattern was unquoted, so an empty or wildcard commit passed the guard.
  • Certified fetch. get_proposal_info is now called with dfx canister call --update. The reply is then certified by the NNS subnet instead of being one replica's query answer. NNS governance has no inspect_message, and get_proposal_info is a plain #[query], so it works as an update call. Checked against mainnet: same JSON as the query.
  • No raw title text on the terminal. The print_* helpers use echo -e, which turns escape sequences into control characters. The canister name (^[a-z0-9_-]+$) and the commit are now validated before either is printed. Malformed values and an unparseable title are printed with printf %q.

Testing

These scripts have no Bazel targets. I tested in a throwaway git repo with a stub dfx that returns a given proposal title, plus the real jq/idl2json:

  • Rejected, with no files modified:
    • titles with $(...) in the canister name (NNS and SNS)
    • empty, wildcard, wrong and multi-line commits
    • unknown canister names
    • escape sequences (raw or as literal \e) in the name, the commit, or an unparseable title. No stray control characters reach the output.
  • Still work: legitimate NNS titles (7- and 40-character commits, hyphenated names such as Cycles-minting) and SNS titles. They produce the same changelog output as before.
  • Shell checks: bash -n passes on bash 5.2 and 3.2 (the macOS default). //pre-commit:shfmt-check passes. shellcheck reports no new warnings.

🤖 Generated with Claude Code

…-to-changelog.sh

add-release-to-changelog.sh derives a canister name and commit ID from an
NNS proposal title and uses them to pick which CHANGELOG.md to rewrite.
That title is free text, and it was handled as if it were trusted:

- get_{nns,sns}_canister_code_location looked the canister name up via
  bash indirect expansion (${!name}). Bash evaluates array subscripts in
  such names, including command substitutions. The lookup is now a case
  statement over the known canister names, and unknown names fail.
- The "is the proposal's commit checked out" guard compared against an
  unquoted glob, so an empty or wildcard commit passed. The commit ID must
  now be 7-40 lowercase hex characters and is compared literally.
- get_proposal_info was fetched with a query, i.e. the answer of a single
  replica. It is now fetched with --update, so the reply is certified.
- Title-derived values were printed via print_* (echo -e), which turns
  escape sequences into terminal control characters. The canister name and
  commit are validated before being printed, and malformed values (and an
  unparseable title) are printed with printf %q.

Output for every known canister name is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are reported for the release changelog tooling hardening changes.

No open issues 🎉

What changed in this PR

This PR hardens the release changelog tooling against untrusted proposal titles before using extracted values to select and rewrite changelog files.

Changes:

  • Replaces indirect canister-name lookups with fixed allowlists.
  • Validates canister names and commit IDs, compares commit prefixes literally, and safely prints malformed input.
  • Fetches proposal information through an update call for a certified response.
File Description
rs/​nervous_system/​tools/​release/​lib/​functions.sh Uses explicit canister mappings and rejects unknown names.
rs/​nervous_system/​tools/​release/​add-release-to-changelog.sh Adds input validation, safe diagnostics, literal commit matching, and certified proposal fetching.

Review effort: Balanced

@pietrodimarco-dfinity
pietrodimarco-dfinity marked this pull request as ready for review November 12, 2026 16:18
@pietrodimarco-dfinity
pietrodimarco-dfinity requested a review from a team as a code owner November 12, 2026 16:18

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request changes code owned by the Governance team. Therefore, make sure that
you have considered the following (for Governance-owned code):

  1. Update unreleased_changelog.md (if there are behavior changes, even if they are
    non-breaking).

  2. Are there BREAKING changes?

  3. Is a data migration needed?

  4. Security review?

How to Satisfy This Automatic Review

  1. Go to the bottom of the pull request page.

  2. Look for where it says this bot is requesting changes.

  3. Click the three dots to the right.

  4. Select "Dismiss review".

  5. In the text entry box, respond to each of the numbered items in the previous
    section, declare one of the following:

  • Done.

  • $REASON_WHY_NO_NEED. E.g. for unreleased_changelog.md, "No
    canister behavior changes.", or for item 2, "Existing APIs
    behave as before.".

Brief Guide to "Externally Visible" Changes

"Externally visible behavior change" is very often due to some NEW canister API.

Changes to EXISTING APIs are more likely to be "breaking".

If these changes are breaking, make sure that clients know how to migrate, how to
maintain their continuity of operations.

If your changes are behind a feature flag, then, do NOT add entrie(s) to
unreleased_changelog.md in this PR! But rather, add entrie(s) later, in the PR
that enables these changes in production.

Reference(s)

For a more comprehensive checklist, see here.

GOVERNANCE_CHECKLIST_REMINDER_DEDUP

@zeropath-ai

zeropath-ai Bot commented Nov 12, 2026

Copy link
Copy Markdown

✅ No security or compliance issues detected. Reviewed everything up to 6e0e604.

Security Overview
Detected Code Changes
Change Type Relevant files
Enhancement ► rs/nervous_system/tools/release/add-release-to-changelog.sh
      Add --update flag to get_proposal_info call and improve parsing/validation of proposal info
► rs/nervous_system/tools/release/lib/functions.sh
      Improve canister code location resolution by switching from indirect variable expansion to explicit case statements for both NNS and SNS canisters
      Add validation for canister names and provide explicit unknown-canister error handling

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for title:

fix(release-tools): Make add-release-to-changelog.sh handle proposal title more robustly.

"untrusted" is confusing, because in general, we are reading in our own proposals, not random proposals.

Comment on lines +28 to +32
#
# --update is used (even though get_proposal_info is a query method) so that
# the reply is certified by the NNS subnet, rather than being the word of
# whichever single replica happens to answer a query. The title of the reply
# determines which files this script rewrites.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last sentence seems unrelated to the rest of the paragraph.

Also, it is hardly telling us anything. It goes without saying that the response will somehow affect the outcome.

if you want to mention that we use the title, ok, but let's throw the reader more of a bone:

We glean the canister and commit from the proposal's title.

Comment on lines +29 to +31
# --update is used (even though get_proposal_info is a query method) so that
# the reply is certified by the NNS subnet, rather than being the word of
# whichever single replica happens to answer a query. The title of the reply

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--update is good, but the first sentence of this paragraph seems superfluous. I think readers will almost certainly find it obvious that --update is more secure.

Comment on lines +147 to +151
# Map of locations. CANISTER_NAME may come from untrusted input (e.g. a
# proposal title), so it is only ever compared against this fixed list.
# Do NOT build a variable name from it and use indirect expansion
# (${!name}): bash evaluates array subscripts in such names, which runs
# any embedded $(...).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Map of locations. CANISTER_NAME may come from untrusted input (e.g. a
# proposal title), so it is only ever compared against this fixed list.
# Do NOT build a variable name from it and use indirect expansion
# (${!name}): bash evaluates array subscripts in such names, which runs
# any embedded $(...).
# Where canister code lives.

Why delete warning: It sort of references old code/bug. Comments should not do that, because it assumes the reader knows about the old buggy code, and they wouldn't know about it.

I mean, yes, "avoid !" is good general advice, but I don't think anyone would look at this case and think, "ah. Let me convert this case to code_location__${canister} (or similar)!". If people tend to make such changes, ok, let's keep this warning, but otherwise, the warning seems spammy.

Comment on lines +181 to +182
# Map of locations. See get_nns_canister_code_location for why this is a
# case statement rather than an indirect variable lookup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment on lines +89 to +94
# Fail if what was extracted from the (untrusted) title is malformed. This
# must happen before either value is printed with print_* (see above for why);
# offending values are printed with printf %q instead.
#
# Proposal titles contain an abbreviated (7 character) commit ID, but a full
# one is also accepted.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Fail if what was extracted from the (untrusted) title is malformed. This
# must happen before either value is printed with print_* (see above for why);
# offending values are printed with printf %q instead.
#
# Proposal titles contain an abbreviated (7 character) commit ID, but a full
# one is also accepted.
# Check bits we gleaned from the title:
# 1. canister
# 2. commit

# untrusted, and print_red uses `echo -e`, which would turn escape
# sequences in it into terminal control characters.
print_red "💀 Unable to parse proposal title:" >&2
printf '%q\n' "${TITLE}" >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to me, the right way is like this:

# Replaces "strange" characters with question marks.
#
# All printable characters are ok. Ditto for newline and tab.
# (Everything else is "strange".)
#
# This should be used when printing external content to the terminal.
# Otherwise, the output might be misleading.
censor_strange_characters() {
    printf '%s' "${*//[^[:print:]$'\n'$'\t']/?}"
}

print_red() {
    printf '%b%s%b\n' "${RED_TEXT}" "$(censor_strange_characters "$@")" "${NO_COLOR}" 1>&2
}

Ditto for other colors, ofc.

# one is also accepted.
if [[ ! "${CANISTER_NAME}" =~ ^[a-z0-9_-]+$ ]]; then
print_red "💀 Malformed canister name in proposal title:" >&2
printf '%q\n' "${CANISTER_NAME}" >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion involving censor_strange_characters could be applied here.

Comment on lines +101 to +102
print_red "💀 Malformed commit ID in proposal title:" >&2
printf '%q\n' "${DESTINATION_COMMIT_ID}" >&2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Comment on lines +114 to +115
# Fail if the proposal's commit is not checked out. The commit ID is quoted so
# that it is compared literally (as a prefix), not as a glob pattern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail if the proposal's commit is not checked out.

Ok, but why? Ofc, this issue existed before, so I will not block on this issue. I'm just bringing this up since you are touching this anyway.

Comment on lines +114 to +115
# Fail if the proposal's commit is not checked out. The commit ID is quoted so
# that it is compared literally (as a prefix), not as a glob pattern.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commit ID is quoted so that it is compared literally (as a prefix), not as a glob pattern.

Again, this seems to be referencing old code/bugs. Do not mention such things. To the vast majority of readers, such comments are very noisy.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants