Back to Blog
Security March 05, 2026 | 8 min read

Zero Trust in Elixir: The SEAL Security Doctrine

Implementing the SEAL security doctrine in Elixir: parameterized Ecto queries, environment variable secrets management, Code.eval prevention, input validation boundaries, and OWASP Top 10 mapping.

Tomas Korcak (korczis)

Prismatic Platform

Why SEAL Exists


Every security breach starts with a violated assumption. SQL injection assumes queries are safely constructed. Hardcoded secrets assume the repository is private. Code evaluation assumes input is trusted. The SEAL (Security Enforcement Absolute Lock) doctrine eliminates these assumptions at the code level, making entire categories of vulnerabilities structurally impossible.


SEAL is not a guideline β€” it is enforced through pre-commit hooks, CI pipeline checks, and AST analysis mix tasks. Code that violates SEAL principles cannot be committed, cannot pass CI, and cannot reach production. This is security-by-construction, not security-by-review.


OWASP Top 10 Mapping


SEAL maps directly to the OWASP Top 10 categories with specific Elixir enforcement patterns:


OWASP CategorySEAL RuleEnforcementDetection

|---------------|-----------|-------------|-----------|

A01: Broken Access ControlRBAC via CasbinRuntime + TestIntegration tests A02: Cryptographic FailuresNo hardcoded secretsPre-commit grepPattern scan A03: InjectionParameterized queries onlyAST analysisfragment/1 scan A04: Insecure DesignInput validation boundariesCode reviewArchitecture review A05: Security MisconfigurationEnvironment-based configConfig validationStartup checks A06: Vulnerable ComponentsDependency auditCI pipelinemix deps.audit A07: Auth FailuresSession managementRuntime + TestAuth module tests A08: Data Integrity Failures:safe binary_to_termPre-commit grepPattern scan A09: Logging FailuresStructured logging mandatoryOTEL doctrineTelemetry audit A10: SSRFURL allowlistingRuntime validationRequest interceptor

Parameterized Ecto Queries


SQL injection through string interpolation in Ecto fragments is the most common Elixir-specific vulnerability. SEAL mandates parameterized queries everywhere:



# BANNED by SEAL β€” SQL injection via string interpolation

defmodule Unsafe do

import Ecto.Query


# This allows arbitrary SQL injection through the value parameter

def search_bad(value) do

from(u in "users", where: fragment("name = '#{value}'"))

end

end


# SEAL-compliant β€” parameterized query

defmodule Prismatic.Accounts.UserQuery do

@moduledoc """

SEAL-compliant user query builder with parameterized fragments.

All dynamic values are passed as query parameters, never interpolated.

"""


import Ecto.Query


@spec search_by_name(Ecto.Queryable.t(), String.t()) :: Ecto.Query.t()

def search_by_name(queryable, name) do

from(u in queryable, where: fragment("name = ?", ^name))

end


@spec search_by_pattern(Ecto.Queryable.t(), String.t()) :: Ecto.Query.t()

def search_by_pattern(queryable, pattern) do

sanitized = sanitize_like_pattern(pattern)

from(u in queryable, where: fragment("name ILIKE ?", ^sanitized))

end


@spec filter_by_ids(Ecto.Queryable.t(), list(integer())) :: Ecto.Query.t()

def filter_by_ids(queryable, ids) when is_list(ids) do

from(u in queryable, where: u.id in ^ids)

end


defp sanitize_like_pattern(pattern) do

pattern

|> String.replace("\\", "\\\\")

|> String.replace("%", "\\%")

|> String.replace("_", "\\_")

|> then(&"%#{&1}%")

end

end


Environment Variable Secrets Management


Hardcoded secrets are detected by pre-commit hooks scanning for common patterns. SEAL mandates all secrets come from environment variables or a vault:



# BANNED by SEAL β€” hardcoded API key

defmodule UnsafeConfig do

def api_key, do: "sk-1234567890abcdef"

end


# SEAL-compliant β€” environment variable with validation

defmodule Prismatic.Config.Secrets do

@moduledoc """

SEAL-compliant secrets management.

All secrets are sourced from environment variables at runtime.

Missing required secrets cause immediate startup failure.

"""


@spec fetch!(String.t()) :: String.t()

def fetch!(env_var) do

case System.get_env(env_var) do

nil ->

raise RuntimeError,

"Required secret #{env_var} not configured. " <>

"Set it as an environment variable before starting the application."


"" ->

raise RuntimeError,

"Secret #{env_var} is set but empty. Provide a valid value."


value ->

value

end

end


@spec fetch(String.t(), String.t() | nil) :: String.t() | nil

def fetch(env_var, default \\ nil) do

System.get_env(env_var) || default

end


@spec configured?(String.t()) :: boolean()

def configured?(env_var) do

case System.get_env(env_var) do

nil -> false

"" -> false

_ -> true

end

end

end


The pre-commit hook that catches hardcoded secrets scans for patterns like API keys, passwords, and tokens:



# Patterns detected by SEAL pre-commit hook

@secret_patterns [

~r/(?:api[_-]?key|secret|password|token)\s[:=]\s["'][a-zA-Z0-9+\/=]{16,}/i,

~r/sk-[a-zA-Z0-9]{20,}/,

~r/ghp_[a-zA-Z0-9]{36}/,

~r/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/,

~r/AKIA[0-9A-Z]{16}/

]


Code.eval Prevention


Code.eval_string/1 executes arbitrary Elixir code at runtime. If user input reaches this function, it enables remote code execution. SEAL bans all forms of runtime code evaluation:



# BANNED by SEAL β€” arbitrary code execution

Code.eval_string(user_input)

Code.eval_quoted(user_ast)

Code.compile_string(user_code)


# SEAL-compliant β€” controlled execution via allowlist

defmodule Prismatic.SafeEval do

@moduledoc """

SEAL-compliant safe expression evaluator.

Only allows pre-approved operations through an explicit allowlist.

No arbitrary code execution is possible.

"""


@allowed_operations %{

"add" => &Kernel.+/2,

"subtract" => &Kernel.-/2,

"multiply" => &Kernel.*/2,

"divide" => &safe_divide/2,

"uppercase" => &String.upcase/1,

"lowercase" => &String.downcase/1

}


@spec evaluate(String.t(), list()) :: {:ok, term()} | {:error, :forbidden_operation}

def evaluate(operation, args) when is_binary(operation) and is_list(args) do

case Map.get(@allowed_operations, operation) do

nil ->

{:error, :forbidden_operation}


func ->

try do

{:ok, apply(func, args)}

rescue

e in [ArithmeticError, FunctionClauseError] ->

{:error, {:evaluation_failed, Exception.message(e)}}

end

end

end


defp safe_divide(_, 0), do: raise(ArithmeticError, "division by zero")

defp safe_divide(a, b), do: a / b

end


Input Validation Boundaries


SEAL enforces input validation at system boundaries β€” the point where external data enters the system. Internal function calls between trusted modules do not need redundant validation:



defmodule Prismatic.Validation.Boundary do

@moduledoc """

Input validation at system boundaries.

Validates and sanitizes all external input before it enters

the trusted internal domain.

"""


@spec validate_email(String.t()) :: {:ok, String.t()} | {:error, String.t()}

def validate_email(input) when is_binary(input) do

trimmed = String.trim(input)


cond do

byte_size(trimmed) > 254 ->

{:error, "Email exceeds maximum length"}


not Regex.match?(~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/, trimmed) ->

{:error, "Invalid email format"}


true ->

{:ok, String.downcase(trimmed)}

end

end


def validate_email(_), do: {:error, "Email must be a string"}


@spec validate_ip(String.t()) :: {:ok, :inet.ip_address()} | {:error, String.t()}

def validate_ip(input) when is_binary(input) do

case :inet.parse_address(String.to_charlist(String.trim(input))) do

{:ok, ip} -> {:ok, ip}

{:error, _} -> {:error, "Invalid IP address format"}

end

end


@spec validate_domain(String.t()) :: {:ok, String.t()} | {:error, String.t()}

def validate_domain(input) when is_binary(input) do

trimmed = String.trim(input) |> String.downcase()


cond do

byte_size(trimmed) > 253 ->

{:error, "Domain exceeds maximum length"}


not Regex.match?(~r/^[a-z0-9]([a-z0-9-][a-z0-9])?(\.[a-z0-9]([a-z0-9-][a-z0-9])?)*$/, trimmed) ->

{:error, "Invalid domain format"}


true ->

{:ok, trimmed}

end

end

end


Enforcement Infrastructure


SEAL enforcement operates at three levels. Pre-commit hooks run grep-based pattern scans that catch the most common violations in under 100ms. CI runs AST-based analysis via mix tasks that detect subtler patterns like indirect Code.eval calls through variable references. Runtime guards in production reject malformed input at system boundaries before it can reach any business logic.


Enforcement LevelToolSpeedCoverage

|------------------|------|-------|----------|

Pre-commitgrep pattern scan< 100msString.to_atom, hardcoded secrets, fragment interpolation CI pipelineAST mix task< 30sCode.eval chains, unsafe binary_to_term, indirect patterns RuntimeBoundary validationPer-requestAll external input at API/LiveView boundaries MonitoringTelemetry + alertsContinuousAnomalous query patterns, auth failures

The SEAL doctrine transforms security from an afterthought into a structural property of the codebase. Violations are impossible to commit, not just discouraged. This is the difference between security policy and security architecture.

Tags

security seal elixir ecto owasp

Related Glossary Terms