lib/sobelow/xss/raw.ex

defmodule Sobelow.XSS.Raw do
  @moduledoc """
  # XSS in `raw`

  This submodule checks for the use of `raw` in templates
  as this can lead to XSS vulnerabilities if taking user input.

  Raw checks can be ignored with the following command:

      $ mix sobelow -i XSS.Raw
  """
  @uid 30
  @finding_type "XSS.Raw: XSS"

  use Sobelow.Finding

  def run(fun, meta_file, _, nil) do
    confidence = if !meta_file.is_controller?, do: :low

    Finding.init(@finding_type, meta_file.filename, confidence)
    |> Finding.multi_from_def(fun, parse_raw_def(fun))
    |> Enum.each(&Print.add_finding(&1))
  end

  def run(fun, meta_file, _web_root, controller) do
    {vars, _, {fun_name, line_no}} = parse_render_def(fun)
    filename = meta_file.filename
    templates = Sobelow.MetaLog.get_templates()

    tmp_template_root =
      templates
      |> Map.keys()
      |> List.first()

    template_root =
      case tmp_template_root do
        nil -> ""
        path -> String.split(path, "/templates/") |> List.first()
      end

    Enum.each(vars, fn {finding, {template, ref_vars, vars}} ->
      template =
        cond do
          is_atom(template) -> Atom.to_string(template) <> ".html"
          is_binary(template) -> template
          true -> ""
        end

      maybe_template_path =
        (template_root <> "/templates/" <> controller <> "/" <> template <> ".eex")
        |> Utils.normalize_path()

      {raw_funs, template_path} = get_rf_tp(templates, maybe_template_path)

      if raw_funs do
        raw_vals = Parse.get_template_vars(raw_funs.raw)

        Enum.each(ref_vars, fn var ->
          var = "@#{var}"

          if Enum.member?(raw_vals, var) do
            Sobelow.MetaLog.delete_raw(var, template_path)
            t_name = String.replace_prefix(Path.expand(template_path, ""), "/", "")
            add_finding(t_name, line_no, filename, fun_name, fun, var, :high, finding)
          end
        end)

        Enum.each(vars, fn var ->
          var = "@#{var}"

          if Enum.member?(raw_vals, var) do
            Sobelow.MetaLog.delete_raw(var, template_path)
            t_name = String.replace_prefix(Path.expand(template_path, ""), "/", "")
            add_finding(t_name, line_no, filename, fun_name, fun, var, :medium, finding)
          end
        end)
      end
    end)
  end

  defp get_rf_tp(templates, template_path) do
    if templates[template_path] do
      {templates[template_path], template_path}
    else
      new_path = String.slice(template_path, 0..(String.length(template_path) - 4)) <> "heex"
      {templates[new_path], new_path}
    end
  end

  def parse_render_def(fun) do
    {params, {fun_name, line_no}} = Parse.get_fun_declaration(fun)

    pipefuns =
      Parse.get_pipe_funs(fun)
      |> Enum.map(fn {_, _, opts} -> Enum.at(opts, 1) end)
      |> Enum.flat_map(&Parse.get_funs_of_type(&1, :render))

    pipevars =
      pipefuns
      |> Enum.map(&{&1, Parse.parse_render_opts(&1, params, 0)})
      |> List.flatten()

    vars =
      (Parse.get_funs_of_type(fun, :render) -- pipefuns)
      |> Enum.map(&{&1, Parse.parse_render_opts(&1, params, 1)})

    {vars ++ pipevars, params, {fun_name, line_no}}
  end

  def parse_raw_def(fun) do
    Parse.get_fun_vars_and_meta(fun, 0, :raw, :HTML)
  end

  defp add_finding(t_name, line_no, filename, fun_name, fun, var, severity, finding) do
    finding =
      %Finding{
        type: @finding_type,
        filename: filename,
        fun_source: fun,
        vuln_source: finding,
        vuln_variable: var,
        vuln_line_no: Parse.get_fun_line(finding),
        vuln_col_no: Parse.get_fun_column(finding),
        confidence: severity
      }
      |> Finding.fetch_fingerprint()

    case Sobelow.format() do
      "json" ->
        json_finding = [
          type: finding.type,
          file: finding.filename,
          variable: "#{finding.vuln_variable}",
          template: "#{t_name}",
          line: finding.vuln_line_no
        ]

        Sobelow.log_finding(json_finding, finding)

      "txt" ->
        Sobelow.log_finding(finding)

        Print.print_custom_finding_metadata(finding, [
          Print.finding_file_name(filename),
          Print.finding_line(finding.vuln_source),
          Print.finding_fun_metadata(fun_name, line_no),
          "Template: #{t_name} - #{var}"
        ])

      "compact" ->
        Print.log_compact_finding(finding)

      "flycheck" ->
        Print.log_flycheck_finding(finding)

      _ ->
        Sobelow.log_finding(finding)
    end
  end
end