Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/security/input_validator.ex
defmodule Raxol.Security.InputValidator do
@moduledoc """
Secure input validation and sanitization module.
Provides comprehensive input validation with security in mind,
preventing common attacks like SQL injection, XSS, and command injection.
"""
alias Raxol.Security.Auditor
@type validation_rule ::
{:type, atom()}
| {:required, boolean()}
| {:min_length, non_neg_integer()}
| {:max_length, non_neg_integer()}
| {:format, Regex.t()}
| {:in, list()}
| {:custom, function()}
@type field_spec :: %{
required(:name) => atom(),
required(:rules) => [validation_rule()],
optional(:sanitize) => boolean(),
optional(:error_message) => String.t()
}
@doc """
Validates a map of inputs against a schema.
## Examples
schema = [
%{name: :username, rules: [{:type, :string}, {:min_length, 3}, {:max_length, 20}]},
%{name: :email, rules: [{:type, :string}, {:format, ~r/^[\\w._%+-]+@[\\w.-]+\\.[A-Za-z]{2,}$/}]},
%{name: :age, rules: [{:type, :integer}, {:min, 18}, {:max, 120}]}
]
validate_inputs(%{username: "john", email: "john@example.com", age: 25}, schema)
"""
def validate_inputs(inputs, schema) when is_map(inputs) and is_list(schema) do
results =
Enum.reduce(schema, {%{}, []}, fn field_spec, {valid, errors} ->
field_name = field_spec.name
value =
Map.get(inputs, field_name) || Map.get(inputs, to_string(field_name))
case validate_field(value, field_spec) do
{:ok, sanitized_value} ->
{Map.put(valid, field_name, sanitized_value), errors}
{:error, reason} ->
{valid, [{field_name, reason} | errors]}
end
end)
case results do
{valid_inputs, []} -> {:ok, valid_inputs}
{_, errors} -> {:error, Enum.reverse(errors)}
end
end
@doc """
Validates a single field against its rules.
"""
def validate_field(value, field_spec) do
with {:ok, value} <- check_required(value, field_spec),
{:ok, value} <- check_type(value, field_spec),
{:ok, value} <- apply_rules(value, field_spec.rules),
{:ok, value} <- sanitize_if_needed(value, field_spec) do
{:ok, value}
end
end
@doc """
Common validation patterns.
"""
def patterns do
%{
email: ~r/^[\w._%+-]+@[\w.-]+\.[A-Za-z]{2,}$/,
url:
~r/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)$/,
phone: ~r/^\+?[1-9]\d{1,14}$/,
alphanumeric: ~r/^[a-zA-Z0-9]+$/,
alpha: ~r/^[a-zA-Z]+$/,
numeric: ~r/^[0-9]+$/,
uuid: ~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
}
end
@doc """
Sanitizes common input types.
"""
def sanitize(value, type) do
case type do
:html -> sanitize_html(value)
:sql -> sanitize_sql(value)
:filename -> sanitize_filename(value)
:url -> sanitize_url(value)
_ -> value
end
end
# Private functions
defp check_required(nil, %{rules: rules}) do
is_required = {:required, true} in rules
handle_required_check(is_required)
end
defp check_required(value, _), do: {:ok, value}
defp handle_required_check(true), do: {:error, "is required"}
defp handle_required_check(false), do: {:ok, nil}
defp check_type(nil, _), do: {:ok, nil}
defp check_type(value, %{rules: rules}) do
case Enum.find(rules, fn
{:type, _} -> true
_ -> false
end) do
{:type, expected_type} -> validate_type(value, expected_type)
nil -> {:ok, value}
end
end
defp validate_type(value, :string) when is_binary(value), do: {:ok, value}
defp validate_type(value, :integer) when is_integer(value), do: {:ok, value}
defp validate_type(value, :float) when is_float(value), do: {:ok, value}
defp validate_type(value, :number) when is_number(value), do: {:ok, value}
defp validate_type(value, :boolean) when is_boolean(value), do: {:ok, value}
defp validate_type(value, :atom) when is_atom(value), do: {:ok, value}
defp validate_type(value, :list) when is_list(value), do: {:ok, value}
defp validate_type(value, :map) when is_map(value), do: {:ok, value}
# Type coercion
defp validate_type(value, :integer) when is_binary(value) do
case Integer.parse(value) do
{int, ""} -> {:ok, int}
_ -> {:error, "must be an integer"}
end
end
defp validate_type(value, :float) when is_binary(value) do
case Float.parse(value) do
{float, ""} -> {:ok, float}
_ -> {:error, "must be a float"}
end
end
defp validate_type(value, :boolean) when is_binary(value) do
case String.downcase(value) do
"true" -> {:ok, true}
"false" -> {:ok, false}
_ -> {:error, "must be a boolean"}
end
end
defp validate_type(_, type), do: {:error, "must be a #{type}"}
defp apply_rules(nil, _), do: {:ok, nil}
defp apply_rules(value, rules) do
Enum.reduce_while(rules, {:ok, value}, fn rule, {:ok, val} ->
case apply_rule(val, rule) do
{:ok, new_val} -> {:cont, {:ok, new_val}}
error -> {:halt, error}
end
end)
end
defp apply_rule(value, {:min_length, min}) when is_binary(value) do
meets_min = String.length(value) >= min
handle_min_length_check(meets_min, value, min)
end
defp apply_rule(value, {:max_length, max}) when is_binary(value) do
meets_max = String.length(value) <= max
handle_max_length_check(meets_max, value, max)
end
defp apply_rule(value, {:min, min}) when is_number(value) do
meets_min = value >= min
handle_min_value_check(meets_min, value, min)
end
defp apply_rule(value, {:max, max}) when is_number(value) do
meets_max = value <= max
handle_max_value_check(meets_max, value, max)
end
defp apply_rule(value, {:format, regex}) when is_binary(value) do
matches = Regex.match?(regex, value)
handle_format_check(matches, value)
end
defp apply_rule(value, {:in, allowed}) do
is_allowed = value in allowed
handle_allowed_check(is_allowed, value, allowed)
end
defp apply_rule(value, {:custom, validator}) when is_function(validator, 1) do
validator.(value)
end
defp apply_rule(value, _), do: {:ok, value}
defp handle_min_length_check(true, value, _min), do: {:ok, value}
defp handle_min_length_check(false, _value, min) do
{:error, "must be at least #{min} characters"}
end
defp handle_max_length_check(true, value, _max), do: {:ok, value}
defp handle_max_length_check(false, _value, max) do
{:error, "must be at most #{max} characters"}
end
defp handle_allowed_check(true, value, _allowed), do: {:ok, value}
defp handle_allowed_check(false, _value, allowed) do
{:error, "must be one of: #{inspect(allowed)}"}
end
defp handle_min_value_check(true, value, _min), do: {:ok, value}
defp handle_min_value_check(false, _value, min) do
{:error, "must be at least #{min}"}
end
defp handle_max_value_check(true, value, _max), do: {:ok, value}
defp handle_max_value_check(false, _value, max) do
{:error, "must be at most #{max}"}
end
defp handle_format_check(true, value), do: {:ok, value}
defp handle_format_check(false, _value), do: {:error, "has invalid format"}
defp sanitize_if_needed(value, %{sanitize: true, rules: rules})
when is_binary(value) do
sanitize_type =
Enum.find_value(rules, fn
{:sanitize_type, type} -> type
_ -> nil
end) || :text
{:ok, sanitize(value, sanitize_type)}
end
defp sanitize_if_needed(value, _), do: {:ok, value}
defp sanitize_html(html) do
Auditor.sanitize_html(html)
end
defp sanitize_sql(value) do
# SQL should use parameterized queries, not sanitization
value
end
defp sanitize_filename(filename) do
filename
|> String.replace(~r/[^a-zA-Z0-9._-]/, "_")
|> String.slice(0, 255)
end
defp sanitize_url(url) do
# Basic URL sanitization
has_protocol = String.starts_with?(url, ["http://", "https://"])
handle_url_protocol(has_protocol, url)
end
defp handle_url_protocol(true, url), do: url
defp handle_url_protocol(false, url), do: "https://#{url}"
@doc """
Creates a validator function for reuse.
## Examples
username_validator = create_validator([
{:type, :string},
{:min_length, 3},
{:max_length, 20},
{:format, ~r/^[a-zA-Z0-9_]+$/}
])
username_validator.("john_doe")
# => {:ok, "john_doe"}
"""
def create_validator(rules, opts \\ []) do
fn value ->
field_spec = %{
name: :field,
rules: rules,
sanitize: Keyword.get(opts, :sanitize, false)
}
validate_field(value, field_spec)
end
end
@doc """
Validates multiple fields in parallel for performance.
"""
def validate_parallel(inputs, schema) do
tasks =
Enum.map(schema, fn field_spec ->
Task.async(fn ->
field_name = field_spec.name
value = Map.get(inputs, field_name)
case validate_field(value, field_spec) do
{:ok, sanitized} -> {:ok, {field_name, sanitized}}
{:error, reason} -> {:error, {field_name, reason}}
end
end)
end)
results = Task.await_many(tasks)
{valid, errors} =
Enum.reduce(results, {%{}, []}, fn
{:ok, {name, value}}, {valid, errors} ->
{Map.put(valid, name, value), errors}
{:error, {name, reason}}, {valid, errors} ->
{valid, [{name, reason} | errors]}
end)
handle_validation_result(Enum.empty?(errors), valid, errors)
end
defp handle_validation_result(true, valid, _errors), do: {:ok, valid}
defp handle_validation_result(false, _valid, errors), do: {:error, errors}
end