Skip to content

parser/unparser: AstToString produces unparseable output for doubles in scientific notation #1325

Description

@moraneus

Summary

cel.AstToString produces a syntactically invalid CEL expression when
the resulting double literal is formatted in scientific notation by
strconv.FormatFloat with 'g' and precision -1 (i.e., for any
double of magnitude < 1e-4 or >= 1e+21). The unparser appends .0
to any formatted double that lacks a . so that it parses as a double
rather than an int, but it does not consider that scientific notation
already produces a valid double literal — appending .0 to 1e-05
produces 1e-05.0, which is rejected by the parser.

Reported via Google OSS-VRP and classified as a product vulnerability
under OT2/OT3 (non-bounty); filing here per the triage team's
suggestion.

Affected code

parser/unparser.go lines 296-302:

case types.Double:
    // represent the float using the minimum required digits
    d := strconv.FormatFloat(float64(val), 'g', -1, 64)
    un.str.WriteString(d)
    if !strings.Contains(d, ".") {
        un.str.WriteString(".0")
    }

Reproducer

package main

import (
      "fmt"

      "github.com/google/cel-go/cel"
)

func main() {
      env, _ := cel.NewEnv()
      for _, src := range []string{"0.00001", "0.000000001", "1e30", "5e-5", "1E7"} {
              ast, iss := env.Parse(src)
              if iss != nil && iss.Err() != nil {
                      fmt.Printf("first parse of %q failed: %v\n", src, iss.Err())
                      continue
              }
              s2, _ := cel.AstToString(ast)
              _, iss = env.Parse(s2)
              fmt.Printf("%-20s -> AstToString=%-15s reparse=", src, fmt.Sprintf("%q", s2))
              if iss == nil || iss.Err() == nil {
                      fmt.Println("OK")
              } else {
                      fmt.Println("FAIL:", iss.Err())
              }
      }
}

Output against v0.28.1:

"0.00001"            -> AstToString="1e-05.0"     reparse=FAIL: ERROR: <input>:1:6: Syntax error: extraneous input '.0' expecting <EOF>
"0.000000001"        -> AstToString="1e-09.0"     reparse=FAIL
"1e30"               -> AstToString="1e+30.0"     reparse=FAIL
"5e-5"               -> AstToString="5e-05.0"     reparse=FAIL
"1E7"                -> AstToString="1e+07.0"     reparse=FAIL

For comparison, doubles inside the 'g' fixed-format range round-trip
correctly (no bug):

"0.0001"             -> AstToString="0.0001"      reparse=OK
"1.5"                -> AstToString="1.5"         reparse=OK

Versions confirmed vulnerable

  • v0.28.1 (latest released at time of report)
  • master @ commit a63c50303c3aec3836b78831c61ad3b0f66ea1ad (2026-05-18)

Suggested fix

Treat scientific notation as also being a valid CEL double literal —
only append .0 if the formatted string has neither a . nor an
exponent marker:

case types.Double:
    d := strconv.FormatFloat(float64(val), 'g', -1, 64)
    un.str.WriteString(d)
    if !strings.ContainsAny(d, ".eE") {
        un.str.WriteString(".0") 
    }

Discovered via differential fuzzing using a Parse → AstToString → Parse
roundtrip oracle. Happy to send a PR if useful — let me know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions