fix(cli): --input-csv Term and PaymentOption fields bypass validateFlags — bad values cause confusing API errors · Issue #379 · LeanerCloud/cloud-commitments-cli · GitHub
Skip to content

fix(cli): --input-csv Term and PaymentOption fields bypass validateFlags — bad values cause confusing API errors #379

Open

Description

@cristim

Summary

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

  1. Run ri-helper --services rds to produce an output CSV.
  2. Edit the CSV: change PaymentOption from no-upfront to no_upfront and Term from 1yr to 1-year.
  3. Run ri-helper --input-csv <file> --purchase.
  4. 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.

func validateCSVRecord(rec common.Recommendation) error {
    validPayment := map[string]bool{"all-upfront": true, "partial-upfront": true, "no-upfront": true}
    if !validPayment[rec.PaymentOption] {
        return fmt.Errorf("invalid PaymentOption %q", rec.PaymentOption)
    }
    if rec.Term != "1yr" && rec.Term != "3yr" {
        return fmt.Errorf("invalid Term %q (expected 1yr or 3yr)", rec.Term)
    }
    if rec.Count <= 0 || rec.Count > MaxReasonableInstances {
        return fmt.Errorf("Count %d out of range [1, %d]", rec.Count, MaxReasonableInstances)
    }
    return nil
}

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.

Activity

  1. cristim commented on Jul 28, 2026

    @cristim
    MemberAuthor

    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.

  2. IAmJSD commented on Oct 20, 2026

    @IAmJSD
    Collaborator

    Claiming: working on this

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions