Packages

Custom Credo checks for common Elixir/Phoenix anti-patterns and CWE Top 25 security vulnerabilities. Provides 40 comprehensive checks covering code quality, error handling, performance, change risk, telemetry, and security (SQL injection, XSS, path traversal, CSRF, SSRF, hardcoded credentials, etc).

Current section

Files

Jump to
oeditus_credo lib credo check security xss_vulnerability.ex
Raw

lib/credo/check/security/xss_vulnerability.ex

defmodule OeditusCredo.Check.Security.XSSVulnerability do
@moduledoc """
Checks for patterns that may allow cross-site scripting (XSS).
MITRE reference: [CWE-79](https://cwe.mitre.org/data/definitions/79.html)
Improper Neutralization of Input During Web Page Generation
("Cross-site Scripting").
"""
use Credo.Check,
base_priority: :higher,
category: :warning,
explanations: [
check: """
Detects potential Cross-Site Scripting (XSS) vulnerabilities ([CWE-79](https://cwe.mitre.org/data/definitions/79.html)).
Rendering unescaped user-controlled HTML with `raw/1` or `{:safe, ...}`
can allow script injection.
Bad:
raw(user_input)
{:safe, user_html}
Good:
# Let Phoenix escape by default
content_tag(:div, user_input)
""",
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
issues_from_ast =
source_file
|> Credo.Code.prewalk(&traverse(&1, &2, issue_meta))
issues_from_lines =
if template_file?(source_file.filename) do
source_file
|> Credo.Code.to_lines()
|> line_scan_for_safe_tuple(issue_meta)
else
[]
end
issues_from_ast ++ issues_from_lines
end
end
@doc false
@impl true
def param_defaults, do: [exclude_test_files: false]
# raw(...)
defp traverse({:raw, meta, _args} = ast, issues, issue_meta) do
{ast, [issue_for(issue_meta, meta[:line], "raw/1 bypasses HTML escaping") | issues]}
end
# Phoenix.HTML.raw(...)
defp traverse(
{{:., _, [{:__aliases__, _, [:Phoenix, :HTML]}, :raw]}, meta, _args} = ast,
issues,
issue_meta
) do
{ast,
[issue_for(issue_meta, meta[:line], "Phoenix.HTML.raw/1 bypasses HTML escaping") | issues]}
end
# {:safe, value}
defp traverse({:{}, meta, [:safe, _value]} = ast, issues, issue_meta) do
{ast,
[issue_for(issue_meta, meta[:line], "{:safe, ...} marks content as trusted HTML") | issues]}
end
defp traverse(ast, issues, _issue_meta), do: {ast, issues}
defp template_file?(filename) do
String.ends_with?(filename, [".heex", ".leex", ".eex"])
end
defp line_scan_for_safe_tuple(lines, issue_meta) do
lines
|> Enum.with_index(1)
|> Enum.flat_map(fn {{_, line}, line_no} ->
if String.contains?(line, "{:safe,") do
[issue_for(issue_meta, line_no, "inline {:safe, ...} in template")]
else
[]
end
end)
end
defp issue_for(issue_meta, line_no, detail) do
format_issue(
issue_meta,
message: "Potential XSS vulnerability (CWE-79): #{detail}.",
trigger: "raw",
line_no: line_no
)
end
end