Packages
Custom Credo checks for common Elixir/Phoenix anti-patterns and CWE Top 25 security vulnerabilities. Provides 58 comprehensive checks in total.
Current section
Files
Jump to
Current section
Files
lib/credo/check/security/missing_csrf_protection.ex
defmodule OeditusCredo.Check.Security.MissingCSRFProtection do
@moduledoc """
Checks for missing or bypassed CSRF protection in web/API flows.
MITRE reference: [CWE-352](https://cwe.mitre.org/data/definitions/352.html) —
Cross-Site Request Forgery (CSRF).
"""
use Credo.Check,
base_priority: :high,
category: :warning,
explanations: [
check: """
Detects potential missing or disabled CSRF protection ([CWE-352](https://cwe.mitre.org/data/definitions/352.html)).
This check covers both:
1) Web/API pipelines handling state-changing routes without CSRF protection.
2) Explicit CSRF removal or bypass patterns.
Bad:
pipeline :api do
plug :accepts, ["json"]
end
Plug.Conn.delete_csrf_token(conn)
Good:
pipeline :browser do
plug :protect_from_forgery
end
""",
params: [
exclude_test_files: "Set to true to skip test files (default: false)"
]
]
import OeditusCredo.Helpers, only: [test_file?: 1]
@doc false
@impl true
def run(%SourceFile{} = source_file, params) do
issue_meta = IssueMeta.for(source_file, params)
if Params.get(params, :exclude_test_files, __MODULE__) and
test_file?(source_file.filename) do
[]
else
source_file
|> Credo.Code.prewalk(&traverse(&1, &2, issue_meta))
end
end
@doc false
@impl true
def param_defaults, do: [exclude_test_files: false]
# pipeline :api do ... end
defp traverse({:pipeline, meta, [pipe_name, [do: body]]} = ast, issues, issue_meta) do
statements = block_to_statements(body)
issues =
if api_pipeline?(pipe_name) and not has_protect_from_forgery?(statements) do
[
issue_for(
issue_meta,
meta[:line],
"API pipeline without :protect_from_forgery (or equivalent CSRF protection)"
)
| issues
]
else
issues
end
{ast, issues}
end
# post/put/patch/delete route declarations outside protected pipeline context
defp traverse({method, meta, _args} = ast, issues, issue_meta)
when method in [:post, :put, :patch, :delete] do
{ast,
[
issue_for(
issue_meta,
meta[:line],
"state-changing route `#{method}` detected; ensure CSRF protection is enabled"
)
| issues
]}
end
# Plug.Conn.delete_csrf_token(conn)
defp traverse(
{{:., _, [{:__aliases__, _, [:Plug, :Conn]}, :delete_csrf_token]}, meta, _args} = ast,
issues,
issue_meta
) do
{ast, [issue_for(issue_meta, meta[:line], "explicit CSRF token deletion detected") | issues]}
end
# put_private(conn, :plug_skip_csrf_protection, true)-style bypass hints
defp traverse({name, meta, args} = ast, issues, issue_meta)
when name in [:put_private, :assign] and is_list(args) do
issues =
if Enum.any?(args, &csrf_bypass_key?/1) do
[issue_for(issue_meta, meta[:line], "potential CSRF bypass flag assignment") | issues]
else
issues
end
{ast, issues}
end
defp traverse(ast, issues, _issue_meta), do: {ast, issues}
defp block_to_statements({:__block__, _, statements}) when is_list(statements), do: statements
defp block_to_statements(statement), do: [statement]
defp api_pipeline?(name) when is_atom(name), do: name == :api
defp api_pipeline?(_), do: false
defp has_protect_from_forgery?(statements) do
Enum.any?(statements, fn
{:plug, _, [plug_name | _rest]} ->
plug_name
|> plug_name_to_string()
|> String.downcase()
|> String.contains?("protect_from_forgery")
_ ->
false
end)
end
defp plug_name_to_string({:__aliases__, _, parts}) when is_list(parts),
do: Enum.join(parts, ".")
defp plug_name_to_string(name) when is_atom(name), do: Atom.to_string(name)
defp plug_name_to_string(_), do: ""
defp csrf_bypass_key?(:plug_skip_csrf_protection), do: true
defp csrf_bypass_key?("plug_skip_csrf_protection"), do: true
defp csrf_bypass_key?(:csrf_disabled), do: true
defp csrf_bypass_key?("csrf_disabled"), do: true
defp csrf_bypass_key?(_), do: false
defp issue_for(issue_meta, line_no, detail) do
format_issue(
issue_meta,
message: "Potential CSRF protection issue (CWE-352): #{detail}.",
trigger: "csrf",
line_no: line_no
)
end
end