fix(cli): --input-csv Term and PaymentOption fields bypass validateFlags — bad values cause confusing API errors · Issue #379 · LeanerCloud/cloud-commitments-cli · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(cli): --input-csv Term and PaymentOption fields bypass validateFlags — bad values cause confusing API errors #379
loadRecommendationsFromCSV() in cmd/multi_service.go:166-230 reads Term and PaymentOption directly from the CSV without any allowlist validation. validateFlags() checks these fields for CLI flags but is not called for CSV-supplied values.
Location
cmd/multi_service.go:218-225 (CSV field parsing for Term and PaymentOption) cmd/multi_service.go:119-241 (validateFlags — validates CLI flags only, not CSV content)
Impact
A user who edits the output CSV and enters a typo (e.g., 3-year instead of 3yr, or partial_upfront instead of partial-upfront) will receive a confusing deep AWS API error like invalid payment option from findOfferingID/convertPaymentOption rather than a clear validation message at load time. The Count field uses fmt.Sscanf without range checks, so a value like 99999 would pass unnoticed and trigger purchases far beyond --max-instances intent (since filterAndAdjustRecommendations only applies MaxInstances as an instance-count cap, not per-recommendation count cap).
This is not a remote code execution risk (the tool runs locally with the user's own credentials), but it violates the principle that --input-csv is a trusted-but-validated input path, and can lead to unintended purchases.
Reproduction
Run ri-helper --services rds to produce an output CSV.
Edit the CSV: change PaymentOption from no-upfront to no_upfront and Term from 1yr to 1-year.
Run ri-helper --input-csv <file> --purchase.
Observe: validateFlags passes (no CLI flags set), but the purchase attempt fails with a cryptic error from deep inside convertPaymentOption.
Suggested Fix
Add a validateCSVRecord helper called from loadRecommendationsFromCSV (or from filterAndAdjustRecommendations) that checks each row's Term and PaymentOption against the same allowlists used in validateFlags, and that validates Count is > 0 and <= MaxReasonableInstances. Log and skip invalid rows with a clear message.
funcvalidateCSVRecord(rec common.Recommendation) error {
validPayment:=map[string]bool{"all-upfront": true, "partial-upfront": true, "no-upfront": true}
if!validPayment[rec.PaymentOption] {
returnfmt.Errorf("invalid PaymentOption %q", rec.PaymentOption)
}
ifrec.Term!="1yr"&&rec.Term!="3yr" {
returnfmt.Errorf("invalid Term %q (expected 1yr or 3yr)", rec.Term)
}
ifrec.Count<=0||rec.Count>MaxReasonableInstances {
returnfmt.Errorf("Count %d out of range [1, %d]", rec.Count, MaxReasonableInstances)
}
returnnil
}
Severity Rationale
severity/low — Affects only users who hand-edit the CSV input, causes confusing errors rather than incorrect purchases (the API rejects bad offering parameters).
Findings from the 2026-09-02 codebase audit
Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.
A10-007 (medium)
The same boundary gap applies to a third column this issue does not name. parseCSVRecord (cmd/multi_service_csv.go:106, the current home of the code) casts an arbitrary Service cell straight to common.ServiceType, bypassing the serviceMap at cmd/main.go:190. Even a valid flag alias such as elasticsearch, or rds with a trailing space, produces a ServiceType that createServiceClient's switch cannot match (cmd/main.go:244-268), so the row is dropped at cmd/multi_service.go:538-541 with 'Service client not yet implemented' rather than an error naming the bad cell. The operator sees a short run and no failure. Worth covering by the same validateCSVRecord helper. One narrowing on the PaymentOption half: every AWS provider converts and errors on an unknown value before any purchase, so the bad value produces a deep error rather than a wrong purchase. Audit finding A10-007.
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
Re-confirmed live during the 2026-07-28 full review of cmd/. Adding current line numbers (the code moved into cmd/multi_service_csv.go since this issue was written) and one piece of evidence that strengthens the case, rather than filing a duplicate.
Current location:cmd/multi_service_csv.go:111-112 assigns Term and PaymentOption straight from the CSV cell with no validation. The flag equivalents are validated in validatePaymentAndTerm (cmd/validators.go:125-143): PaymentOption must be one of three strings, TermYears must be 1 or 3.
Evidence that the gap is known and unclosed:warnRDS3YearNoUpfront explicitly acknowledges the CSV case and skips its own check because of it (cmd/validators.go:147-152):
in CSV-input mode the payment option comes from each row, not the --payment flag
So the code already knows the per-row values exist and are unvalidated, and nothing validates them instead.
Additional failure mode not in the issue text: the audit log silently records a wrong term. A CSV produced by a spreadsheet or a third-party export carries PaymentOption = "Partial Upfront" (title case with a space) and Term = "3 years". Both flow through unvalidated into executePurchase -> PurchaseCommitment and into the audit and report fields. termMonths (pkg/common/audit.go:74-86) does not recognise "3 years" and records term_months: 0 in the audit log. So the run does not simply fail with a confusing error: the durable record of what was attempted is wrong too, and the reconciliation workflow described at pkg/common/audit.go:23-26 has no way to recover the real term.
The worse branch is where the string is close enough to resolve: the provider offering lookup may match a different offering than the one the operator read off the CSV, in which case a valid purchase is made on terms the operator never approved.
Fix direction (refines the one in this issue): validate Term and PaymentOption per row inside parseCSVRecord against the same allow-lists validatePaymentAndTerm uses, and fail the whole load on the first bad row rather than logging and skipping. Skipping a row on a purchase path silently narrows the run, which is the same class of problem.
Related: the Count half of this issue's "Suggested Fix" is tracked in more detail on #1327 (fmt.Sscanf truncation, negative counts, blank cells); a comment there from this same review pins those line numbers.
Activity
cristim commentedon Jul 28, 2026
Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have been fixed or moved.Re-confirmed live during the 2026-07-28 full review of
cmd/. Adding current line numbers (the code moved intocmd/multi_service_csv.gosince this issue was written) and one piece of evidence that strengthens the case, rather than filing a duplicate.Current location:
cmd/multi_service_csv.go:111-112assignsTermandPaymentOptionstraight from the CSV cell with no validation. The flag equivalents are validated invalidatePaymentAndTerm(cmd/validators.go:125-143):PaymentOptionmust be one of three strings,TermYearsmust be 1 or 3.Evidence that the gap is known and unclosed:
warnRDS3YearNoUpfrontexplicitly acknowledges the CSV case and skips its own check because of it (cmd/validators.go:147-152):So the code already knows the per-row values exist and are unvalidated, and nothing validates them instead.
Additional failure mode not in the issue text: the audit log silently records a wrong term. A CSV produced by a spreadsheet or a third-party export carries
PaymentOption = "Partial Upfront"(title case with a space) andTerm = "3 years". Both flow through unvalidated intoexecutePurchase->PurchaseCommitmentand into the audit and report fields.termMonths(pkg/common/audit.go:74-86) does not recognise"3 years"and recordsterm_months: 0in the audit log. So the run does not simply fail with a confusing error: the durable record of what was attempted is wrong too, and the reconciliation workflow described atpkg/common/audit.go:23-26has no way to recover the real term.The worse branch is where the string is close enough to resolve: the provider offering lookup may match a different offering than the one the operator read off the CSV, in which case a valid purchase is made on terms the operator never approved.
Fix direction (refines the one in this issue): validate
TermandPaymentOptionper row insideparseCSVRecordagainst the same allow-listsvalidatePaymentAndTermuses, and fail the whole load on the first bad row rather than logging and skipping. Skipping a row on a purchase path silently narrows the run, which is the same class of problem.Related: the
Counthalf of this issue's "Suggested Fix" is tracked in more detail on #1327 (fmt.Sscanftruncation, negative counts, blank cells); a comment there from this same review pins those line numbers.IAmJSD commentedon Oct 20, 2026
Claiming: working on this