Skip to content

NewCachingCompiler does not preserve CEL string escape semantics #53

Description

@liby

Summary

NewCachingCompiler can change the meaning of valid CEL string literals. It lifts the raw source text between quotes into vars, while CEL normally decodes escapes when parsing the literal. Once the rewritten expression references vars.a, that value bypasses CEL string decoding.

As a result, direct CEL evaluation and cached/lifted evaluation of the same expression can return different results.

Reproduction

func TestCachingCompile_StringEscapeSemantics(t *testing.T) {
    env := newEnv()
    expression := `event.value.matches("^prefix\\/suffix$")`
    input := map[string]any{
        "event": map[string]any{"value": "prefix/suffix"},
    }

    directAST, directIssues := env.Compile(expression)
    require.Nil(t, directIssues)
    directProgram, err := env.Program(directAST)
    require.NoError(t, err)
    direct, _, err := directProgram.Eval(input)
    require.NoError(t, err)
    require.Equal(t, true, direct.Value())

    cachedAST, cachedIssues, vars := NewCachingCompiler(env, nil).Compile(expression)
    require.Nil(t, cachedIssues)
    cachedProgram, err := env.Program(cachedAST)
    require.NoError(t, err)
    input[VarPrefix] = vars.Map()
    cached, _, err := cachedProgram.Eval(input)
    require.NoError(t, err)

    require.Equal(t, direct.Value(), cached.Value())
}

On current main (6e2f3dcb86677f1b54fbda7eb8cb80f8f224d012), the direct result is true and the cached result is false.

Root cause

consumeString skips over escape pairs while scanning, but argMapValue.get later returns the original source slice unchanged. The rewritten AST references vars.a, so cel-go never parses that value as a literal and never applies CEL unescaping.

The scanner change in #46 prevents incorrect string termination, but it intentionally keeps the raw source bytes and therefore does not preserve literal semantics.

Suggested fix

Do not lift string literals that contain escape sequences. Leaving those literals in the rewritten expression lets cel-go remain the source of truth for CEL unescaping and validation, while ordinary unescaped literals can still be lifted and cached as they are today.

Please add parity coverage for escaped backslashes, quotes, newlines, and Unicode escapes.

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