Repository navigation
fix(api): reject control characters in account fields and fail closed in federation IaC render - #1006
Conversation
… in federation IaC render A line break in a value that a federation template prints on a "#" comment line ends the comment, and the rest runs as shell (or HCL in a tfvars file). shellEscape and hclEscape escape quoting metacharacters but not line breaks, and the account write paths accepted CR/LF in name and external_id. - validateCloudAccountRequest (account create, update, self-account and registration approval) rejects any control character in name and external_id with a 400. - validateRegistrationRequest keeps stripping CR/LF from account_name and now rejects any other control character there and any control character in external_id, so a submission that could never be approved fails at submit time. - renderTemplate refuses data with a control character other than tab in any string field, so stored or future bad data produces an error instead of a script that runs an injected command. Closes LeanerCloud#970 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 6 billable files and costs up to $1.50. Or wait 58 minutes for your next included review. View limit details
Comment |
Independent review verdict: CLEAN at 96f1aafReviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief, acting as a security reviewer. The PR body was treated as a claim. Coordinator: claude-cc-reviewer. Blocking findings: none for the #970 scope. Every rendered template variable was traced (
Non-blocking notes:
What was attacked and how:
Gate:
|
|
I merged 🤖 Generated with Claude Code |
|
Head moved to b202bee: a merge of origin/main (173474e, picks up #1003) into the reviewed 96f1aaf. The merge commit's tree is identical to |
|
Gate record for b202bee (origin/main CLAUDE.md "Review gate"):
Merging at b202bee with --match-head-commit. |
|
Gate record for b202bee (origin/main CLAUDE.md "Review gate"):
Merging at b202bee with --match-head-commit. |
Summary
A CR or LF in a value that a federation template prints on a
#comment line ends the comment, and the rest of the value runs as shell, or as HCL in a tfvars file.shellEscapeandhclEscapeescape quoting metacharacters but not line breaks. The account write paths also accepted CR and LF innameandexternal_id.Closes #970
What reproduces on main
Checked on main (
0ed85aa3, which already includes #955 and #992) before changing anything:POST /api/accountsthrough the realcreateAccounthandler accepts{"name":"Acme\ntouch PWNED","external_id":"x\ntouch PWNED"}with no error.PUTgoes through the samevalidateCloudAccountRequest.renderTemplate("templates/azure-wif-cli.sh.tmpl", shellEscapeData(data))withAccountName = "Acme\ntouch PWNED\n#"producesGET /api/federation/iacdoes not put stored account fields into the templates.buildGenericIaCDataleavesAccountName/AccountExternalIDempty,AccountSlugis the constanttarget, andSourcecomes from an allowlist. So the server download is not exploitable through account fields right now, which answers the issue's "not verified end to end" note. The only per-request value that reaches the templates is the session email, andmail.ParseAddressrejects CR/LF in it.CUDlyAPIURLcomes from config or the LambdaDomainName; the HTTP adapter never setsDomainName.scripts/generate-federation-iac.goalready rejects every flag value that contains a control character (checkNoControlChars,unicode.IsControl, from fix(scripts): escape flag values per output format in generate-federation-iac #955), so it needs no change here.The weakness is still real: the write paths store the bad value, and the renderer turns it into a working injection as soon as anything passes that value in. The fix closes both sides.
Fix
The fix rejects bad values rather than escaping or stripping them. There is no escape that keeps a line break inside a
#comment. Silently stripping characters would change an identifier such as an external ID without telling anyone.validateCloudAccountRequest(create, update, self-account, registration approval)unicode.IsControl: C0 including CR/LF/tab, DEL, C1 such as NEL) innameorexternal_idgets a 400 naming the field.validateRegistrationRequest(publicPOST /api/register)account_name(#544, unchanged). Now also rejects any other control character inaccount_nameand any control character inexternal_id. Approval would reject these anyway, so they now fail at submit time.renderTemplate(every template:*.sh,*.tfvars,*.jsonparams)executeTemplate.Template/field matrix
The sandbox oracle finds these line-break injections when the guard is removed (
executeTemplate, LF payload, shell escaping applied).renderTemplaterefuses all of them:aws-cfn-deploy.shaws-cross-account-cfn-deploy.shaws-cross-account-cli.shaws-wif-cli.shazure-wif-cli.shgcp-sa-impersonation-cli.shgcp-wif-cli.sh*.tfvars(all five)azure-wif-deploy.sh,azure-wif-bicep-deploy.shCompatibility with #955 / #992
#992 (
fix/963-followups) is already merged and is part of this branch's base.git merge-tree --write-tree HEAD fork/fix/963-followupsis clean (exit 0, treed5df88aa), and so is the merge withorigin/main. The tab exemption keeps #992's"a\tb"@example.comcase working, andTestGetFederationIaC_AzureDeployScriptEscapesSessionEmailpasses.Verification
All tests ran with
AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null, on macOS.New tests:
handler_federation_controlchar_test.goTestRenderTemplate_RefusesControlCharsInEveryField: every embedded template × every string field × LF, CRLF, CR, VT, FF, NUL, ESC, DEL, NEL, with the handler's escaping applied. Each render is refused, the error names the field, and there is no output.TestRenderTemplate_UnguardedInjectionIsCaughtByOracle: shows the oracle is not vacuous. Without the guard, the marker runs andbash -nstill accepts the script. The issue's fields must be detected, and each detected combination must be refused byrenderTemplate. Sandbox: empty temp working directory,HOMEset to it, andPATHholding only a symlink totouch, so no cloud CLI orcurlis reachable.TestRenderTemplate_UnguardedTfvarsInjection: the HCL counterpart for the five tfvars templates.TestRenderTemplate_BenignDataUnchanged: guarded output is byte-identical to unguarded output for every template, including a tab in the email, and every rendered script passesbash -n.TestGetFederationIaC_FailsClosedOnControlChar: drives the real handler over all 27 valid target/source/format combinations. Benign requests succeed. A session email containing LF gets a non-4xx error and no content.handler_accounts_controlchar_test.go:createAccountandupdateAccountthrough the real handlers (LF, CRLF, CR, TAB, NUL, ESC, DEL, NEL innameand inexternal_id) each return 400, and the store is never called (AssertNotCalledon a plain localMockConfigStore). Printable and non-ASCII names are still accepted unchanged.submitRegistrationthrough the handler rejects these too, and the CR/LF strip still works.Fail-before / pass-after:
Mutation probes, all killed:
name: theexternal_idtests failBenign output vs main: all 90 target/source/format/domain combinations went through the handler on main and on this branch, and the zip entries were extracted and compared. Everything is identical except the per-request random external-ID UUID in
cf-params.json. The same 36 combinations error on both (combinations the server doesn't offer).CI-equivalent checks:
go build ./...andgo vet ./...: clean.go test ./internal/api/... ./internal/iacfiles/... ./scripts/... ./internal/mocks/... -count=1: passes exceptTestAuthPublicRoutes_EveryRouteIsCSRFGuardedOrExempt, which also fails on main. That test fails becausePOST /api/purchases/approve/*/detailsfrom fix(approval): let email-link approvers load purchase details without view:purchases #990 is unclassified. It is unrelated to this change and this PR doesn't touch it.--allow-parallel-runners: 0 issues.gocyclo -over 10on the changed non-test files: none over 10 (max isvalidateRegistrationRequestat 9).-S warning: clean on all 33 rendered scripts.--register-awsandsetup-git-secrets.sh, and it came back clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL.Out of scope
CloudAccountRequestfields such asdescription(may legitimately be multi-line) and the provider-specific IDs are not used on IaC comment lines.🤖 Generated with Claude Code