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