lib/sobelow/xss/send_resp.ex

defmodule Sobelow.XSS.SendResp do
  @moduledoc """
  # XSS in `send_resp`

  This submodule looks for XSS vulnerabilities in the `body`
  argument of `Conn.send_resp`.

  SendResp checks can be ignored with the following command:

      $ mix sobelow -i XSS.SendResp
  """
  @uid 31
  @finding_type "XSS.SendResp: XSS in `send_resp`"

  use Sobelow.Finding

  def run(fun, meta_file) do
    Finding.init(@finding_type, meta_file.filename, nil)
    |> Finding.multi_from_def(fun, parse_def(fun))
    |> Stream.map(&set_confidence/1)
    |> Stream.reject(&nil_confidence?/1)
    |> Enum.each(&Print.add_finding(&1))
  end

  def parse_def(fun) do
    Parse.get_fun_vars_and_meta(fun, 2, :send_resp, :Conn)
  end

  @doc false
  def get_content_type({:put_resp_content_type, _, opts}), do: hd(opts)
  def get_content_type({{_, _, [_, :put_resp_content_type]}, _, opts}), do: hd(opts)

  @doc false
  def set_confidence(%Finding{} = finding) do
    content_types =
      finding.fun_source
      |> Parse.get_funs_of_type(:put_resp_content_type)
      |> Kernel.++(
        Parse.get_aliased_funs_of_type(finding.fun_source, :put_resp_content_type, :Conn)
      )
      |> Enum.map(&get_content_type/1)

    %{finding | confidence: get_confidence(finding, content_types)}
  end

  defp get_confidence(finding, content_types) do
    cond do
      Enum.empty?(content_types) ->
        finding.confidence

      Enum.any?(content_types, &(!is_binary(&1))) ->
        :low

      Enum.all?(content_types, &contains_html?/1) ->
        finding.confidence

      Enum.any?(content_types, &contains_html?/1) ->
        :low

      true ->
        nil
    end
  end

  defp contains_html?(content_type) do
    content_type
    |> String.downcase()
    |> String.contains?("html")
  end

  @doc false
  def nil_confidence?(%Finding{confidence: nil}), do: true
  def nil_confidence?(_), do: false
end