unsafe_exec.ex (2000B)
1 defmodule Credo.Check.Warning.UnsafeExec do 2 use Credo.Check, 3 base_priority: :high, 4 category: :warning, 5 explanations: [ 6 check: """ 7 Spawning external commands can lead to command injection vulnerabilities. 8 9 Use a safe API where arguments are passed as an explicit list, rather 10 than unsafe APIs that run a shell to parse the arguments from a single 11 string. 12 13 Safe APIs include: 14 15 * `System.cmd/2,3` 16 * `:erlang.open_port/2`, passing `{:spawn_executable, file_name}` as the 17 first parameter, and any arguments using the `:args` option 18 19 Unsafe APIs include: 20 21 * `:os.cmd/1,2` 22 * `:erlang.open_port/2`, passing `{:spawn, command}` as the first 23 parameter 24 25 """ 26 ] 27 28 @doc false 29 @impl true 30 def run(%SourceFile{} = source_file, params \\ []) do 31 issue_meta = IssueMeta.for(source_file, params) 32 33 Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) 34 end 35 36 defp traverse({{:., _loc, call}, meta, args} = ast, issues, issue_meta) do 37 case get_forbidden_call(call, args) do 38 {bad, suggestion} -> 39 {ast, issues_for_call(bad, suggestion, meta, issue_meta, issues)} 40 41 nil -> 42 {ast, issues} 43 end 44 end 45 46 defp traverse(ast, issues, _issue_meta) do 47 {ast, issues} 48 end 49 50 defp get_forbidden_call([:os, :cmd], [_]) do 51 {":os.cmd/1", "System.cmd/2,3"} 52 end 53 54 defp get_forbidden_call([:os, :cmd], [_, _]) do 55 {":os.cmd/2", "System.cmd/2,3"} 56 end 57 58 defp get_forbidden_call([:erlang, :open_port], [{:spawn, _}, _]) do 59 {":erlang.open_port/2 with `:spawn`", ":erlang.open_port/2 with `:spawn_executable`"} 60 end 61 62 defp get_forbidden_call(_, _) do 63 nil 64 end 65 66 defp issues_for_call(call, suggestion, meta, issue_meta, issues) do 67 options = [ 68 message: "Prefer #{suggestion} over #{call} to prevent command injection", 69 trigger: call, 70 line_no: meta[:line] 71 ] 72 73 [format_issue(issue_meta, options) | issues] 74 end 75 end