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
Current section
Files
lib/credo/check/security/sensitive_data_exposure.ex
defmodule OeditusCredo.Check.Security.SensitiveDataExposure do
@moduledoc """
Checks for logging/output patterns that may expose sensitive information.
MITRE reference: [CWE-200](https://cwe.mitre.org/data/definitions/200.html) --
Exposure of Sensitive Information to an Unauthorized Actor.
"""
use Credo.Check,
base_priority: :high,
category: :warning,
explanations: [
check: """
Detects potential sensitive data exposure in logs or console output ([CWE-200](https://cwe.mitre.org/data/definitions/200.html)).
Logging sensitive fields (passwords, tokens, secrets, credentials) can leak
confidential data into log storage and observability systems.
Bad:
Logger.info("params: \#{inspect(params)}")
IO.inspect(user.password_hash)
Good:
Logger.info("user login", user_id: user.id)
""",
params: [
exclude_test_files: "Set to true to skip test files (default: false)",
extra_sensitive_terms:
"Additional sensitive field name substrings to detect (default: [])"
]
]
@default_sensitive_terms ~w[
password passwd pwd password_hash secret token api_key apikey
access_token refresh_token credential credentials private_key
secret_key ssn credit_card cvv cvc pin otp jwt bearer
]
@logging_mods [[:Logger], [:IO]]
@logging_functions ~w[debug info warning warn error inspect puts]
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
extra = Params.get(params, :extra_sensitive_terms, __MODULE__)
terms = @default_sensitive_terms ++ extra
source_file
|> Credo.Code.prewalk(&traverse(&1, &2, {issue_meta, terms}))
end
end
@doc false
@impl true
def param_defaults, do: [exclude_test_files: false, extra_sensitive_terms: []]
# Logger.info(...) / IO.inspect(...)
defp traverse(
{{:., _, [{:__aliases__, _, mod_parts}, fun]}, meta, args} = ast,
issues,
{issue_meta, terms}
)
when is_list(mod_parts) and is_atom(fun) and is_list(args) do
if logging_call?(mod_parts, fun) and args_contain_sensitive_data?(args, terms) do
{ast, [issue_for(issue_meta, meta[:line], "#{Enum.join(mod_parts, ".")}.#{fun}") | issues]}
else
{ast, issues}
end
end
defp traverse(ast, issues, _ctx), do: {ast, issues}
defp logging_call?(mod_parts, fun) do
mod_parts in @logging_mods and Atom.to_string(fun) in @logging_functions
end
defp args_contain_sensitive_data?(args, terms) do
Enum.any?(args, &contains_sensitive_data?(&1, terms))
end
defp contains_sensitive_data?({:inspect, _, inner_args}, terms) when is_list(inner_args) do
Enum.any?(inner_args, &contains_sensitive_data?(&1, terms))
end
defp contains_sensitive_data?({{:., _, [_left, :inspect]}, _, inner_args}, terms)
when is_list(inner_args) do
Enum.any?(inner_args, &contains_sensitive_data?(&1, terms))
end
defp contains_sensitive_data?({{:., _, [left, attr]}, _, _args}, terms) when is_atom(attr) do
contains_sensitive_data?(left, terms) or sensitive_name?(Atom.to_string(attr), terms)
end
defp contains_sensitive_data?({:., _, [_left, attr]}, terms) when is_atom(attr) do
sensitive_name?(Atom.to_string(attr), terms)
end
defp contains_sensitive_data?({:<<>>, _, parts}, terms) when is_list(parts) do
Enum.any?(parts, fn
part when is_binary(part) -> sensitive_name?(part, terms)
{_, _, _} = ast -> contains_sensitive_data?(ast, terms)
_ -> false
end)
end
defp contains_sensitive_data?({name, _, _args}, terms) when is_atom(name) do
sensitive_name?(Atom.to_string(name), terms)
end
defp contains_sensitive_data?({_, _, args}, terms) when is_list(args) do
Enum.any?(args, &contains_sensitive_data?(&1, terms))
end
defp contains_sensitive_data?(str, terms) when is_binary(str), do: sensitive_name?(str, terms)
defp contains_sensitive_data?(_, _terms), do: false
defp sensitive_name?(name, terms) when is_binary(name) do
down = String.downcase(name)
Enum.any?(terms, &String.contains?(down, &1))
end
defp issue_for(issue_meta, line_no, trigger) do
format_issue(
issue_meta,
message:
"Potential sensitive data exposure (CWE-200): logging/output call references sensitive fields.",
trigger: trigger,
line_no: line_no
)
end
end