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/hardcoded_credentials.ex
defmodule OeditusCredo.Check.Security.HardcodedCredentials do
@moduledoc """
Checks for hard-coded secrets and credential-like values in source code.
MITRE reference: [CWE-798](https://cwe.mitre.org/data/definitions/798.html) --
Use of Hard-coded Credentials.
"""
use Credo.Check,
base_priority: :high,
category: :warning,
explanations: [
check: """
Detects hardcoded credentials and sensitive endpoints ([CWE-798](https://cwe.mitre.org/data/definitions/798.html)).
Hardcoded secrets in source code are vulnerable to leaks and accidental
exposure through repositories, logs, and error reports.
This check extends previous hardcoded value detection by adding explicit
credential-name-based checks (password, secret, token, api_key, etc.).
Bad:
@api_key "sk_live_..."
password = "super-secret"
token = "abc123"
Better:
api_key = System.fetch_env!("API_KEY")
password = Application.fetch_env!(:my_app, :password)
""",
params: [
exclude_test_files: "Set to true to skip test files (default: false)",
extra_credential_terms: "Additional credential name substrings to flag (default: [])"
]
]
@url_pattern ~r/https?:\/\/[^\s"']+/
@ip_pattern ~r/\b(?:\d{1,3}\.){3}\d{1,3}\b/
@default_credential_terms ~w[
password passwd pwd secret token api_key apikey access_token refresh_token
private_key secret_key credential credentials auth_key
]
import OeditusCredo.Helpers, only: [test_file?: 1]
@doc false
@impl true
def run(%SourceFile{}, false), do: []
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_credential_terms, __MODULE__)
terms = @default_credential_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_credential_terms: []]
# Detect literal strings for URL/IP patterns
defp traverse({:<<>>, meta, [string]} = ast, issues, {issue_meta, _terms})
when is_binary(string) do
issues =
cond do
Regex.match?(@url_pattern, string) and not localhost?(string) ->
[issue_for(issue_meta, meta[:line], "hardcoded URL") | issues]
Regex.match?(@ip_pattern, string) and not local_ip?(string) ->
[issue_for(issue_meta, meta[:line], "hardcoded IP address") | issues]
true ->
issues
end
{ast, issues}
end
# Detect assignment: password = "..."
defp traverse({:=, meta, [left, right]} = ast, issues, {issue_meta, terms}) do
if credential_name?(left, terms) and literal_string?(right) do
{ast, [issue_for(issue_meta, meta[:line], "hardcoded credential value") | issues]}
else
{ast, issues}
end
end
# Detect module attributes: @api_key "..."
defp traverse({:@, meta, [{attr_name, _, args}]} = ast, issues, {issue_meta, terms})
when is_atom(attr_name) and is_list(args) do
if credential_name?(attr_name, terms) and Enum.any?(args, &literal_string?/1) do
{ast,
[issue_for(issue_meta, meta[:line], "hardcoded credential module attribute") | issues]}
else
{ast, issues}
end
end
defp traverse(ast, issues, _ctx), do: {ast, issues}
defp credential_name?({name, _, _}, terms) when is_atom(name),
do: credential_name?(name, terms)
defp credential_name?(name, terms) when is_atom(name) do
down = name |> Atom.to_string() |> String.downcase()
Enum.any?(terms, &String.contains?(down, &1))
end
defp credential_name?(_, _terms), do: false
defp literal_string?({:<<>>, _, [str]}) when is_binary(str), do: true
defp literal_string?(str) when is_binary(str), do: true
defp literal_string?(_), do: false
defp localhost?(url) do
String.contains?(url, "localhost") or String.contains?(url, "127.0.0.1")
end
defp local_ip?(ip) do
String.starts_with?(ip, "127.") or String.starts_with?(ip, "192.168.") or
String.starts_with?(ip, "10.") or ip == "0.0.0.0"
end
defp issue_for(issue_meta, line_no, type) do
format_issue(
issue_meta,
message:
"Potential hardcoded credential (CWE-798): #{type}. Use runtime configuration instead.",
trigger: type,
line_no: line_no || 1
)
end
end