Repository navigation
fix(iac): keep '}' and JSON metacharacters in the Bicep/ARM contact email intact (#963 follow-ups) - #992
Conversation
…mail intact (LeanerCloud#958) deploy-azure.sh pre-filled the contact email as the default of ${CUDLY_CONTACT_EMAIL:-...}. A "}" in the email (net/mail accepts it) closed the expansion early and reordered the value, e.g. a}b@example.com became ab}@example.com. Assign the env value first and fall back to the pre-filled email on its own line; the override semantics are unchanged (a set, non-empty CUDLY_CONTACT_EMAIL wins). The self-registration heredoc also put CONTACT_EMAIL and ACCOUNT_NAME into the JSON raw, so a '"', '\' or tab (all accepted by net/mail in a quoted local part) produced a malformed payload. Add a json_escape helper (printf + sed, portable to macOS bash 3.2 and Linux, no jq) and use the escaped values in the heredoc. The other heredoc values are the subscription, tenant and client GUIDs. Tests run the registration block in the sandboxed bash with az stubbed, for hostile emails with "}", "$", backticks, quotes, backslashes and a tab, and check that CONTACT_EMAIL round-trips, that the payload parses as JSON with the exact email, and that nothing executes. They also pin the env-override cases and JSON-escaping of CUDLY_ACCOUNT_NAME. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 3 billable files and costs up to $0.75. Or wait 59 minutes for your next included review. View limit details
Comment |
cristim
left a comment
There was a problem hiding this comment.
Independent review of df91517: no actionable findings
Findings
None BLOCKING. One NON-BLOCKING note on #964 below.
NON-BLOCKING, issue bookkeeping: open issue #964 (unescaped deploy-azure.sh render) is already fixed on main by #963. internal/api/handler_federation.go:561 renders the template with shellEscapeData(data). This PR does not change that. What it adds is the residual } case (#991) and JSON escaping of the payload. Suggest closing #964 as done, since #992 does not close it either (the PR closes only #991).
What I attacked
- Rendered
deploy-azure.shfor both bicep and arm with 11 emails, each valid for the auth service:a}b,"a\"b\\c$HOME", backtick and$(...)substitutions, a}combined with$(...)and backticks,', tab,&|;, trailing backslash,%s,!#. Checkedbash -non each (all pass). - Ran each script end to end in
env -iwith only a stubazand a stubcurlfirst on PATH, an empty cwd, and dummy template files. All 22 runs exited 0. The captured curl-dbody parsed as JSON, andcontact_emailequaled the input byte for byte. No marker file was created (no command ran). - Consistency with the other escapers:
shellEscape(handler_federation.go:399) is unchanged and still correct for the other templates.}is not added to it, which is right because\}is literal inside double quotes. The${VAR:-default}form is gone from this template, so no escaper has to handle}here.hclEscape(#955) targets a different format and is not touched. json_escape(template lines 74-78): thesedexpressions are POSIX, the tab pattern comes fromprintf '\t', and the replacement has no&. CR/LF is not handled, and the helper comment says so honestly. The email cannot hold them (net/mail), andCUDLY_ACCOUNT_NAMEis operator-supplied. A newline in that env var would give invalid JSON and a failed registration, not an injection.- Override semantics: unset, empty and set
CUDLY_CONTACT_EMAILbehave as${VAR:-default}did (tests plus my runs). - Fail-before: restored the parent template with the new tests.
TestGetFederationIaC_AzureDeployScriptEscapesSessionEmailfails for 16 of 18 subtests (theaidbcase has no}/quote/tab, and the first backtick case fails on the JSON escape), andRegistrationEnvOverridesfails. At the SHA,go test ./internal/api -run 'Azure|Federation'gives 314 passed. Reverted, worktree clean. - Scope: 3 files, +120/-28, no other call site of
ContactEmailin the template. The 8 sibling templates are explicitly deferred to #991.
Gate status (origin/main Review gate)
- Independent adversarial review naming the SHA: this comment.
- CI on the SHA: not complete at review time. 8 pass, 13 pending, 0 failed, 0 action_required.
- CodeRabbit: no actionable threads from me to count (not pinged).
- Labels: match #991 (
triaged,priority/p1,severity/high,urgency/this-sprint,impact/few,effort/xs,type/security). - Local verification: real bash runs of the rendered script with stubbed
az/curl(macOS), fail-before/pass-after shown. No real Azure account was used. - Merge condition: all CI green on this SHA.
|
Gate record for df91517 (origin/main CLAUDE.md "Review gate"):
Merging at df91517 with --match-head-commit. |
Closes #991. This follows up #963 (issue #958), which merged at 7a71710 before this commit landed. The commit is cherry-picked unchanged onto main (adcd80c).
Changes (
azure-wif-bicep-deploy.sh.tmpland its tests)}in the email.CONTACT_EMAIL="${CUDLY_CONTACT_EMAIL:-}"and then[ -n "$CONTACT_EMAIL" ] || CONTACT_EMAIL="{{.ContactEmail}}". So the rendered value is never inside a${...:-default}.CUDLY_CONTACT_EMAILwins.shellEscapeis unchanged. Adding}there would corrupt every other template, because\}stays literal inside double quotes.json_escapehelper (printf | sed, no jq) escapes\and", and turns a tab into\t. net/mail accepts a tab in a quoted local part and rejects every other control character.contact_email, and alsoaccount_name, sinceCUDLY_ACCOUNT_NAMEcomes from the operator. The other heredoc values are IDs.Tests
These run in a bare bash: PATH only, an empty temp directory,
set -euo pipefail, andazstubbed. Each runs the block fromCONTACT_EMAIL=through the end of the heredoc and stops beforecurl.mail.ParseAddress. They includea}b,"a}b","a}$(touch pwned)\touch pwned`}b","{a}${b}", a tab, and the earlier$(), backtick and"/\` cases.CONTACT_EMAILcomes back exactly; the payload parses withjson.Unmarshaland has the exactcontact_email; no marker file is created; andbash -npasses on the whole script.RegistrationEnvOverrides: covers the override being unset, empty and set, plus a hostileCUDLY_ACCOUNT_NAME.Verification
All tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null. Everything here is local; noaz,curlor network calls.go test ./internal/api/ -run 'Federation|AzureTemplate|Azure' -count=1 -vgave 310 PASS, 0 FAIL, 1 SKIP. The skip is the existingTfvarsAutoLoadedByTerraform/aws-cross-account. It also passes on this branch.a\id`b, which contains no}`, quote or tab), as do the three env-override subtests.CONTACT_EMAILin the heredoc"and tab case failsaccount_nameunescaped${VAR:-default}form restored}case fails\case failsgo vet ./internal/api/is clean, golangci-lint v2.10.1 reports 0 issues, and gofmt is clean.bash scripts/test-git-secrets-allowlist.shgives 42 PASS, 0 FAIL.Out of scope
Maintainer: please apply labels to #991 and this PR. #958's suggested set is
type/security,type/bug,priority/p2,severity/medium.🤖 Generated with Claude Code