
Elixir Security Review
- 87 installs
- 74 repo stars
- Updated July 21, 2026
- existential-birds/beagle
Helps with security tasks.
About
elixir-security-review is a Claude Code skill for security. It helps solo builders move faster with AI-assisted development.
- elixir-security-review
- Security
- AI-coding skill
Elixir Security Review by the numbers
- 87 all-time installs (skills.sh)
- Ranked #1,067 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/existential-birds/beagle --skill elixir-security-reviewAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 87 |
|---|---|
| repo stars | ★ 74 |
| Last updated | July 21, 2026 |
| Repository | existential-birds/beagle ↗ |
What it does
Helps with security tasks.
Files
Elixir Security Review
Quick Reference
| Issue Type | Reference |
|---|---|
| Code.eval_string, binary_to_term | references/code-injection.md |
| String.to_atom dangers | references/atom-exhaustion.md |
| Config, environment variables | references/secrets.md |
| ETS visibility, process dictionary | references/process-exposure.md |
Review Checklist
Critical (Block Merge)
- [ ] No
Code.eval_string/1on user input - [ ] No
:erlang.binary_to_term/1without:safeon untrusted data - [ ] No
String.to_atom/1on external input - [ ] No hardcoded secrets in source code
Major
- [ ] ETS tables use appropriate access controls
- [ ] No sensitive data in process dictionary
- [ ] No dynamic module creation from user input
- [ ] Path traversal prevented in file operations
Configuration
- [ ] Secrets loaded from environment
- [ ] No secrets in config/*.exs committed to git
- [ ] Runtime config used for deployment secrets
Valid Patterns (Do NOT Flag)
- String.to_atom on compile-time constants - Atoms created at compile time are safe
- Code.eval_string in dev/test - May be needed for tooling
- ETS :public tables - Valid when intentionally shared
- binary_to_term with :safe - Explicitly safe option used
Context-Sensitive Rules
| Issue | Flag ONLY IF |
|---|---|
| String.to_atom | Input comes from external source (user, API, file) |
| binary_to_term | Data comes from untrusted source |
| ETS :public | Contains sensitive data |
Hard gates (before reporting)
Complete in order for each finding you intend to report. Do not advance until the pass condition is satisfied.
1. Location artifact — The finding includes [FILE:LINE] (or a line range) that you copied from the current file contents; the path resolves in this repo. 2. Scope read — You read the full surrounding function or module section that contains the flagged code, not only a diff hunk or summary. 3. External-data claim (only if the finding depends on “user/untrusted input”) — You can name one concrete ingress (for example conn.params, Jason.decode!/1 result, uploaded file path, message from another node) or you drop the finding because the value is compile-time, test-only, or internal per Context-Sensitive Rules. 4. Protocol — Pre-report steps in review-verification-protocol are satisfied for this item (no finding if they are not).
Before Submitting Findings
Use the issue format: [FILE:LINE] ISSUE_TITLE for each finding.
Hard gate 4 requires review-verification-protocol; use it as the full pre-report checklist and issue-type verification (it extends beyond this skill’s summary).
Atom Exhaustion
The Problem
Atoms are never garbage collected. The VM has a limit (default ~1M atoms). Creating atoms from user input can crash the system.
# VULNERABILITY - DoS via atom exhaustion
def process_field(field_name) do
key = String.to_atom(field_name) # Each unique input creates new atom
Map.get(data, key)
end
# Attacker sends: field_1, field_2, ... field_1000000 -> VM crashDangerous Functions
# NEVER use on external input:
String.to_atom(user_input)
List.to_atom(user_input)
:erlang.binary_to_atom(user_input, :utf8)
:erlang.list_to_atom(user_input)Safe Alternatives
String.to_existing_atom
# Only converts to atoms that already exist
def process_field(field_name) do
try do
key = String.to_existing_atom(field_name)
Map.get(data, key)
rescue
ArgumentError -> {:error, :invalid_field}
end
endWhitelist Approach
@valid_fields ~w(name email phone)a
def process_field(field_name) do
atom = String.to_existing_atom(field_name)
if atom in @valid_fields do
{:ok, Map.get(data, atom)}
else
{:error, :invalid_field}
end
rescue
ArgumentError -> {:error, :invalid_field}
endUse Strings as Keys
# SAFE - no atom creation
def process_json(json_map) do
# JSON keys are already strings
name = Map.get(json_map, "name")
email = Map.get(json_map, "email")
%{name: name, email: email}
endAtom Whitelist in Module Attribute
defmodule API do
@allowed_actions [:create, :read, :update, :delete]
def dispatch(action_string) do
action = String.to_existing_atom(action_string)
if action in @allowed_actions do
perform(action)
else
{:error, :unauthorized_action}
end
rescue
ArgumentError -> {:error, :invalid_action}
end
endSafe Contexts
Atom creation is safe when:
- Input is compile-time constant
- Input comes from trusted internal source
- Input is validated against whitelist first
# SAFE - compile-time
@fields [:name, :email]
# SAFE - internal message
def handle_info({:internal, action}, state) when is_atom(action) do
# action is already an atom from trusted code
endReview Questions
1. Is String.to_atom used on any external input? 2. Is there a whitelist for dynamic atom conversion? 3. Could String.to_existing_atom be used instead? 4. Would using strings as keys work instead?
Code Injection
Code.eval_string
The Danger
# CRITICAL VULNERABILITY
def calculate(user_expression) do
{result, _} = Code.eval_string(user_expression)
result
end
# Attacker input: "System.cmd(\"rm\", [\"-rf\", \"/\"])"Safe Alternatives
1. Parse and validate expressions:
# Safe math expression parser
defmodule SafeMath do
def evaluate(expr) when is_binary(expr) do
with {:ok, tokens} <- tokenize(expr),
{:ok, ast} <- parse(tokens),
:ok <- validate_ast(ast) do
{:ok, eval_ast(ast)}
end
end
defp validate_ast({op, _, _}) when op in [:+, :-, :*, :/], do: :ok
defp validate_ast(n) when is_number(n), do: :ok
defp validate_ast(_), do: {:error, :invalid_expression}
end2. Whitelist allowed operations:
@allowed_ops %{
"add" => &Kernel.+/2,
"subtract" => &Kernel.-/2,
"multiply" => &Kernel.*/2
}
def calculate(op, a, b) when is_map_key(@allowed_ops, op) do
@allowed_ops[op].(a, b)
end:erlang.binary_to_term
The Danger
# CRITICAL VULNERABILITY
def deserialize(data) do
:erlang.binary_to_term(data) # Can create atoms, execute code!
endMalicious binary can:
- Create unlimited atoms (DoS)
- Reference functions that get called
- Contain malicious data structures
Safe Alternative
# SAFE - only allows existing atoms, no function references
def deserialize(data) do
:erlang.binary_to_term(data, [:safe])
rescue
ArgumentError -> {:error, :invalid_term}
endPrefer JSON for External Data
# External API data - use JSON
def parse_api_response(body) do
Jason.decode(body)
end
# Internal Erlang-to-Erlang - binary_to_term with :safe may be ok
def parse_internal_message(data) do
:erlang.binary_to_term(data, [:safe])
endDynamic Module Creation
The Danger
# DANGEROUS
def load_handler(module_name) do
module = String.to_existing_atom("Elixir.Handlers.#{module_name}")
module.handle()
end
# Attacker: "../../System" -> calls System.handle() if existsSafe Pattern
@handlers %{
"email" => Handlers.Email,
"sms" => Handlers.SMS
}
def load_handler(name) do
case Map.fetch(@handlers, name) do
{:ok, module} -> module.handle()
:error -> {:error, :unknown_handler}
end
endReview Questions
1. Is Code.eval_string used on any external input? 2. Is binary_to_term used without :safe option? 3. Are modules loaded dynamically from user input? 4. Is there a whitelist for dynamic operations?
Process Exposure
ETS Visibility
Access Levels
# :public - any process can read/write
# :protected - owner writes, anyone reads (default)
# :private - only owner can access
# DANGEROUS if contains sensitive data
:ets.new(:sessions, [:public]) # Any process can read sessions!
# BETTER - protected access
:ets.new(:sessions, [:protected]) # Only owner can writeSensitive Data in ETS
# BAD - tokens visible to all processes
:ets.insert(:cache, {:user_123, %{token: "secret_token"}})
# GOOD - store reference, not secret
:ets.insert(:cache, {:user_123, %{token_id: "ref_abc"}})
# Actual token in secure storage with access controlsProcess Dictionary
Dangers
The process dictionary is:
- Visible via
Process.info(pid, :dictionary) - Included in crash reports
- Not access controlled
# BAD - secret in process dictionary
Process.put(:api_token, "secret123")
# After crash, token visible in error reports!Safe Alternatives
# Use GenServer state (not in crash reports by default)
defmodule SecureWorker do
use GenServer
def init(token) do
{:ok, %{token: token}} # In state, not dictionary
end
end
# Or dedicated secret storage
defmodule Vault do
def store(key, secret) do
# Encrypted storage or external secret manager
end
endRegistered Process Names
Enumerable
# All registered names are visible
Process.registered() # Returns list of all registered names
# Don't encode secrets in names
# BAD
Process.register(self(), :"worker_secret_token_abc123")
# GOOD
Process.register(self(), :worker_1)Observer / Remote Shell
In production:
- Observer can inspect all processes
- Remote shell has full access
- Limit who can connect
# Restrict remote shell in production
config :my_app, MyAppWeb.Endpoint,
server: true
# Use firewall rules to limit epmd/distribution portsCrash Reports
Sensitive Data Redaction
# Custom formatting to redact secrets
defmodule MyApp.ErrorReporter do
def format_state(state) do
state
|> Map.update(:token, "[REDACTED]", fn _ -> "[REDACTED]" end)
|> Map.update(:password, "[REDACTED]", fn _ -> "[REDACTED]" end)
end
end
# In GenServer
def format_status(_reason, [_pdict, state]) do
[data: [{'State', MyApp.ErrorReporter.format_state(state)}]]
endReview Questions
1. Do ETS tables with sensitive data use :private? 2. Is sensitive data stored in process dictionary? 3. Are crash reports configured to redact secrets? 4. Is production remote access properly restricted?