defmodule Sobelow.Config do
@moduledoc """
# Configuration
Submodules contained within this vulnerability type
are related to common insecurities found in how
Phoenix applications are configured.
This can include things like missing headers,
insecure cookies, and more.
If you wish to learn more about the specific vulnerabilities
found within the Configuration category, you may run the
following commands to find out more:
$ mix sobelow -d Config.CSP
$ mix sobelow -d Config.CSRF
$ mix sobelow -d Config.CSRFRoute
$ mix sobelow -d Config.CSWH
$ mix sobelow -d Config.Headers
$ mix sobelow -d Config.Secrets
$ mix sobelow -d Config.HTTPS
$ mix sobelow -d Config.HSTS
Configuration checks of all types can be ignored with the
following command:
$ mix sobelow -i Config
"""
alias Sobelow.Config.CSP
alias Sobelow.Config.CSRF
alias Sobelow.Config.CSRFRoute
alias Sobelow.Config.CSWH
alias Sobelow.Config.Headers
alias Sobelow.Parse
@submodules [
Sobelow.Config.CSRF,
Sobelow.Config.CSRFRoute,
Sobelow.Config.Headers,
Sobelow.Config.CSP,
Sobelow.Config.Secrets,
Sobelow.Config.HTTPS,
Sobelow.Config.HSTS,
Sobelow.Config.CSWH
]
use Sobelow.FindingType
@skip_files ["dev.exs", "test.exs", "dev.secret.exs", "test.secret.exs"]
def fetch(root, router, endpoints) do
allowed = @submodules -- Sobelow.get_ignored()
ignored_files = Sobelow.get_env(:ignored_files)
dir_path = root <> "config/"
Enum.each(allowed, fn mod ->
cond do
mod in [CSRF, CSRFRoute, Headers, CSP] ->
Enum.each(router, fn path ->
apply(mod, :run, [relative_path(path, root)])
end)
mod in [CSWH] ->
Enum.each(endpoints, fn path ->
apply(mod, :run, [relative_path(path, root)])
end)
File.dir?(dir_path) ->
configs =
File.ls!(dir_path)
|> Enum.filter(&want_to_scan?(dir_path <> &1, ignored_files))
apply(mod, :run, [dir_path, configs])
true ->
nil
end
end)
end
defp want_to_scan?(conf, ignored_files) do
if Path.extname(conf) === ".exs" && !Enum.member?(@skip_files, Path.basename(conf)) &&
!Enum.member?(ignored_files, Path.expand(conf)),
do: conf
end
defp relative_path(path, root) do
path = Path.relative_to(path, Path.expand(root))
case Path.type(path) do
:absolute -> path
_ -> root <> path
end
end
def get_configs_by_file(secret, file) do
if File.exists?(file) do
get_configs(secret, file)
else
[]
end
end
# Config utils
def get_pipelines(filepath) do
ast = Parse.ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &Parse.get_funs_of_type(&1, &2, :pipeline))
acc
end
def get_plug_list(block) do
case block do
{:__block__, _, list} -> list
{_, _, _} = list -> [list]
_ -> []
end
|> Enum.reject(fn {type, _, _} -> type !== :plug end)
end
def is_vuln_pipeline?({:pipeline, _, [_name, [do: block]]}, :csrf) do
plugs = get_plug_list(block)
has_csrf? = Enum.any?(plugs, &is_plug?(&1, :protect_from_forgery))
has_session? = Enum.any?(plugs, &is_plug?(&1, :fetch_session))
has_session? and not has_csrf?
end
def is_vuln_pipeline?({:pipeline, _, [_name, [do: block]]}, :headers) do
plugs = get_plug_list(block)
has_headers? = Enum.any?(plugs, &is_plug?(&1, :put_secure_browser_headers))
accepts = Enum.find_value(plugs, &get_plug_accepts/1)
!has_headers? && is_list(accepts) && Enum.member?(accepts, "html")
end
def get_plug_accepts({:plug, _, [:accepts, {:sigil_w, _, opts}]}), do: parse_accepts(opts)
def get_plug_accepts({:plug, _, [:accepts, accepts]}), do: accepts
def get_plug_accepts(_), do: []
def parse_accepts([{:<<>>, _, [accepts | _]}, []]), do: String.split(accepts, " ")
def is_plug?({:plug, _, [type]}, type), do: true
def is_plug?({:plug, _, [type, _]}, type), do: true
def is_plug?(_, _), do: false
def get_fuzzy_configs(key, filepath) do
ast = Parse.ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &extract_fuzzy_configs(&1, &2, key))
acc
end
def get_configs(key, filepath) do
ast = Parse.ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &extract_configs(&1, &2, key))
acc
end
defp extract_fuzzy_configs({:config, _, opts} = ast, acc, key) when is_list(opts) do
opt = List.last(opts)
vals = if Keyword.keyword?(opt), do: fuzzy_keyword_get(opt, key), else: nil
if is_nil(vals) do
{ast, acc}
else
{ast, [{ast, vals} | acc]}
end
end
defp extract_fuzzy_configs(ast, acc, _key) do
{ast, acc}
end
defp extract_configs({:config, _, opts} = ast, acc, key) when is_list(opts) do
opt = List.last(opts)
val = if Keyword.keyword?(opt), do: Keyword.get(opt, key), else: nil
if is_nil(val) do
{ast, acc}
else
{ast, [{ast, key, val} | acc]}
end
end
defp extract_configs(ast, acc, _key) do
{ast, acc}
end
defp fuzzy_keyword_get(opt, key) do
keys = Keyword.keys(opt)
Enum.map(keys, fn k ->
if is_atom(k) && k != :secret_key_base do
s = Atom.to_string(k) |> String.downcase()
if String.contains?(s, key), do: {k, Keyword.get(opt, k)}
end
end)
|> Enum.reject(&is_nil/1)
end
def get_version(filepath) do
ast = Parse.ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &get_version(&1, &2))
acc
end
def get_version({:@, _, nil} = ast, acc), do: {ast, acc}
def get_version({:@, _, [{:version, _, [vsn]}]}, _acc) when is_binary(vsn), do: {vsn, vsn}
def get_version(ast, acc), do: {ast, acc}
def details do
@moduledoc
end
end