Files
Isaiah Vita 271c6cd99a Limit exponent size in parse_bytes/parse_units to prevent timeout bypass (#8524)
Timeouts passed via context are not honored because builtins that
parse exponents dont include context and will run with unrestricted
size on the exponents. Added an exponent limit checker based
on rough benchmakrking data of whats a reasonabile limit.

Fixes: #8326

Signed-off-by: Isaiah Vita <vita.isaiah@gmail.com>
2026-04-23 13:41:16 +00:00

130 lines
3.0 KiB
Go

// Copyright 2022 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package topdown
import (
"encoding/json"
"fmt"
"math/big"
"strings"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/topdown/builtins"
)
// Binary Si unit constants are borrowed from topdown/parse_bytes
const siMilli = 0.001
const (
siK uint64 = 1000
siM = siK * 1000
siG = siM * 1000
siT = siG * 1000
siP = siT * 1000
siE = siP * 1000
)
func parseUnitsError(msg string) error {
return fmt.Errorf("%s: %s", ast.UnitsParse.Name, msg)
}
func errUnitNotRecognized(unit string) error {
return parseUnitsError(fmt.Sprintf("unit %s not recognized", unit))
}
var (
errNoAmount = parseUnitsError("no amount provided")
errNumConv = parseUnitsError("could not parse amount to a number")
errIncludesSpaces = parseUnitsError("spaces not allowed in resource strings")
errUnitsExponentTooLarge = parseUnitsError("exponent too large")
)
// Accepts both normal SI and binary SI units.
func builtinUnits(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
var x big.Rat
raw, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return err
}
// We remove escaped quotes from strings here to retain parity with units.parse_bytes.
s := string(raw)
s = strings.ReplaceAll(s, "\"", "")
if strings.Contains(s, " ") {
return errIncludesSpaces
}
num, unit, err := extractNumAndUnit(s)
if err != nil {
return errUnitsExponentTooLarge
}
if num == "" {
return errNoAmount
}
// Unlike in units.parse_bytes, we only lowercase after the first letter,
// so that we can distinguish between 'm' and 'M'.
if len(unit) > 1 {
lower := strings.ToLower(unit[1:])
unit = unit[:1] + lower
}
switch unit {
case "m":
x.SetFloat64(siMilli)
case "":
x.SetUint64(none)
case "k", "K":
x.SetUint64(siK)
case "ki", "Ki":
x.SetUint64(ki)
case "M":
x.SetUint64(siM)
case "mi", "Mi":
x.SetUint64(mi)
case "g", "G":
x.SetUint64(siG)
case "gi", "Gi":
x.SetUint64(gi)
case "t", "T":
x.SetUint64(siT)
case "ti", "Ti":
x.SetUint64(ti)
case "p", "P":
x.SetUint64(siP)
case "pi", "Pi":
x.SetUint64(pi)
case "e", "E":
x.SetUint64(siE)
case "ei", "Ei":
x.SetUint64(ei)
default:
return errUnitNotRecognized(unit)
}
numRat, ok := new(big.Rat).SetString(num)
if !ok {
return errNumConv
}
numRat.Mul(numRat, &x)
// Cleaner printout when we have a pure integer value.
if numRat.IsInt() {
return iter(ast.NumberTerm(json.Number(numRat.Num().String())))
}
// When using just big.Float, we had floating-point precision
// issues because quantities like 0.001 are not exactly representable.
// Rationals (such as big.Rat) do not suffer this problem, but are
// more expensive to compute with in general.
return iter(ast.NumberTerm(json.Number(numRat.FloatString(10))))
}
func init() {
RegisterBuiltinFunc(ast.UnitsParse.Name, builtinUnits)
}