Skip to content

Security: Add support for string keys - #6

Merged
dkuku merged 3 commits into
dkuku:masterfrom
bruce:bruce-string-keys
Aug 13, 2026
Merged

Security: Add support for string keys#6
dkuku merged 3 commits into
dkuku:masterfrom
bruce:bruce-string-keys

Conversation

@bruce

@bruce bruce commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

👋 Hello, Daniel!

I'm evaluating using Celixir for CEL conditions in a big project I'm working on—needless to say, I'm very excited that you've put this together!

One thing I've run into: in our project, the variable data we'd bind comes from user-submitted (untrusted) data stored in JSON, which means all keys are strings (in fact, the condition itself might be user-supplied in some cases). We'd love to pass this data directly to Celixir without worrying that the keys would be converted to atoms as part of the process.

Warning

The ERLEF Secure Coding guidelines note that converting untrusted strings to atoms risks atom table exhaustion.

Since this is a common situation (JSON data, Ecto JSON columns, and external APIs all produce string-keyed maps, of course), I'd like to propose some changes to support string keys in Celixir, while keeping fully backward compatible support for atoms.

# String-keyed maps now work directly
Celixir.eval("severity == 'high' && count > 2", %{"severity" => "high", "count" => 3})
# => {:ok, true}

# Atom-keyed maps continue to work exactly as before
Celixir.eval("severity == 'high'", %{severity: "high"})
# => {:ok, true}

Please take a look and let me know what you think! This feels like a good usability and security improvement, and I'd love to work with you to make it viable for contribution to the library.


What Changed

The internal representation for variables and locals in Environment is
now %{String.t() => any()}. Atom-keyed input maps are normalized to strings
at the boundary via Atom.to_string/1 (safe direction, no atom creation).

Key design decisions

  1. Strings as canonical internal representation. CEL identifiers from the
    parser are already strings. Storing variables as strings eliminates conversion
    at every identifier resolution, so a net performance win.

  2. Indexed variable pool in the compiler. The Elixir AST requires atom
    variable names (Macro.var/2). Rather than calling String.to_atom on CEL
    identifiers (which would allow atom exhaustion via crafted expressions), each
    free variable is assigned a positional atom (:__cel_var_0__, :__cel_var_1__,
    etc.). The atom count is bounded by variables-per-expression, not by the
    variety of identifiers across all expressions.

Backward Compatibility

Should be fully backwards-compatible.

  • Environment.new(%{x: 5}); atom keys still work (converted to strings at boundary)
  • put_variable(:name, value); atom names still accepted
  • get_variable(env, :name); atom lookups converted to string internally
  • All existing tests pass unmodified; added string_keys_test.exs for string-specific tests

Extras

  • Took care of an existing unused variable variable warning in runtime.ex
  • mix format run

bruce added 3 commits July 2, 2026 23:52
- Switches Environment internals from atom-keyed to string-keyed maps.
- Atom-keyed input is normalized to strings at the boundary via
Atom.to_string/1. The compiler uses an indexed variable pool
(:__cel_var_0__, :__cel_var_1__, ...) instead of converting CEL
identifier names to atoms.
- No atoms are created from either variable data or expression
identifiers, preventing atom exhaustion from untrusted input on
both paths.

This should be backwards-compatible; atom-keyed and
string-keyed maps are accepted transparently and the tests
still pass, unchanged.

Signed-off-by: Bruce Williams <brwcodes@gmail.com>
Verifies that string-keyed maps work end-to-end through eval, compile,
to_fun, comprehensions, and container resolution.

Signed-off-by: Bruce Williams <brwcodes@gmail.com>
Adds examples of string-keyed variable bindings, reasoning.

Signed-off-by: Bruce Williams <brwcodes@gmail.com>
@bruce
bruce force-pushed the bruce-string-keys branch from ef04475 to 0093baf Compare July 3, 2026 00:50
@bruce

bruce commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Please note that I force-pushed to the branch to ensure commits were signed-off to meet the DCO check.

@bruce

bruce commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@dkuku Just checking to see if you have any questions, and/or follow-up work that I can do.

@dkuku

dkuku commented Aug 12, 2026

Copy link
Copy Markdown
Owner

@bruce Thanks, I saw this while cycling and then forgot to reply. I will release a new version soon.

@dkuku
dkuku merged commit 5e17d09 into dkuku:master Aug 13, 2026
1 check passed
dkuku added a commit that referenced this pull request Aug 13, 2026
Ports the boundary value-encoding onto the string-keyed variables map
introduced in #6. Atom values are converted to strings, recursing through
plain maps and lists, so %{role: :admin} matches role == 'admin'.
Reserved atoms (nil/true/false/:nan/:infinity/:neg_infinity) and structs
pass through untouched.

Key normalization and value encoding are fused into a single pass over
the input map rather than two.

Also restores the to_string_key/1 fallback clause so non-atom, non-binary
keys (integers, charlists) are accepted as before rather than raising
FunctionClauseError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dkuku

dkuku commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Hey @bruce - I released https://celixir.hexdocs.pm/0.4.0-rc1 with few more commits. But I did not have time to test it thus the rc1.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants