You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
20 KiB

(*
* Copyright (c) 2016-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
module L = Logging
let parse_clang_procedure procedure kinds index =
try Some (QualifiedCppName.Match.of_fuzzy_qual_names [procedure], kinds, index)
with QualifiedCppName.ParseError _ ->
(* Java and Clang sources/sinks live in the same inferconfig entry. If we try to parse a Java
procedure that happens to be an invalid Clang qualified name (e.g., MyClass.<init>),
parsing will crash. In the future, we can avoid this by requiring JSON source/sink
specifications to indicate the language *)
None
module SourceKind = struct
type t =
| CommandLineFlag of (Var.t * Typ.desc) (** source that was read from a command line flag *)
| Endpoint of (Mangled.t * Typ.desc) (** source originating from formal of an endpoint *)
| EnvironmentVariable (** source that was read from an environment variable *)
| ReadFile (** source that was read from a file *)
| Other (** for testing or uncategorized sources *)
| UserControlledEndpoint of (Mangled.t * Typ.desc)
(** source originating from formal of an endpoint that is known to hold user-controlled data *)
[@@deriving compare]
let matches ~caller ~callee = Int.equal 0 (compare caller callee)
let of_string = function
| "CommandLineFlag" ->
L.die UserError "User-specified CommandLineFlag sources are not supported"
| "Endpoint" ->
Endpoint (Mangled.from_string "NONE", Typ.Tvoid)
| "EnvironmentVariable" ->
EnvironmentVariable
| "ReadFile" ->
ReadFile
| "UserControlledEndpoint" ->
Endpoint (Mangled.from_string "NONE", Typ.Tvoid)
| _ ->
Other
let external_sources =
List.filter_map
~f:(fun {QuandaryConfig.Source.procedure; kinds; index} ->
parse_clang_procedure procedure kinds index )
(QuandaryConfig.Source.of_json Config.quandary_sources)
(* return a list of source kinds if [procedure_name] is in the list of externally specified sources *)
let get_external_source qualified_pname =
let return = None in
List.concat_map external_sources ~f:(fun (qualifiers, kinds, index) ->
if QualifiedCppName.Match.match_qualifiers qualifiers qualified_pname then
let source_index = try Some (int_of_string index) with Failure _ -> return in
List.rev_map kinds ~f:(fun kind -> (of_string kind, source_index))
else [] )
let get ~caller_pname:_ pname actuals tenv =
let return = None in
match pname with
| Typ.Procname.ObjC_Cpp cpp_name -> (
let qualified_pname = Typ.Procname.get_qualifiers pname in
match
( QualifiedCppName.to_list
(Typ.Name.unqualified_name (Typ.Procname.ObjC_Cpp.get_class_type_name cpp_name))
, Typ.Procname.get_method pname )
with
| ( ["std"; ("basic_istream" | "basic_iostream")]
, ("getline" | "read" | "readsome" | "operator>>") ) ->
[(ReadFile, Some 1)]
| _ ->
get_external_source qualified_pname )
| Typ.Procname.C _ when Typ.Procname.equal pname BuiltinDecl.__global_access -> (
(* is this var a command line flag created by the popular C++ gflags library for creating
command-line flags (https://github.com/gflags/gflags)? *)
let is_gflag access_path =
let pvar_is_gflag pvar =
String.is_substring ~substring:"FLAGS_" (Pvar.get_simplified_name pvar)
in
match access_path with
| (Var.ProgramVar pvar, _), _ ->
Pvar.is_global pvar && pvar_is_gflag pvar
| _ ->
false
in
(* accessed global will be passed to us as the only parameter *)
match List.map actuals ~f:HilExp.ignore_cast with
| [HilExp.AccessExpression access_expr] ->
let access_path = HilExp.AccessExpression.to_access_path access_expr in
if is_gflag access_path then
let (global_pvar, _), _ = access_path in
let typ_desc =
match AccessPath.get_typ access_path tenv with
| Some {Typ.desc} ->
desc
| None ->
Typ.void_star.desc
in
[(CommandLineFlag (global_pvar, typ_desc), None)]
else []
| _ ->
[] )
| Typ.Procname.C _ -> (
match Typ.Procname.to_string pname with
| "getenv" ->
[(EnvironmentVariable, return)]
| _ ->
get_external_source (Typ.Procname.get_qualifiers pname) )
| Typ.Procname.Block _ ->
[]
| pname ->
L.(die InternalError) "Non-C++ procname %a in C++ analysis" Typ.Procname.pp pname
let get_tainted_formals pdesc tenv =
if PredSymb.equal_access (Procdesc.get_attributes pdesc).ProcAttributes.access PredSymb.Private
then Source.all_formals_untainted pdesc
else
let overrides_service_method pname tenv =
PatternMatch.override_exists
(function
| Typ.Procname.ObjC_Cpp cpp_pname ->
let class_name = Typ.Procname.ObjC_Cpp.get_class_name cpp_pname in
let res =
String.is_suffix ~suffix:"SvIf" class_name
|| String.is_suffix ~suffix:"SvAsyncIf" class_name
in
res
| _ ->
false )
tenv pname
in
(* taint all formals except for [this] *)
let taint_all_but_this_and_return ~make_source =
List.map
~f:(fun (name, typ) ->
let taint =
match Mangled.to_string name with
| "this" | "_return" ->
(* thrift methods implement returning values using dummy _return parameters that
the C++ code assigns to. these are sinks, not sources *)
None
| _ ->
Some (make_source name typ.Typ.desc)
in
(name, typ, taint) )
(Procdesc.get_formals pdesc)
in
match Procdesc.get_proc_name pdesc with
| Typ.Procname.ObjC_Cpp cpp_pname as pname ->
let qualified_pname =
F.sprintf "%s::%s"
(Typ.Procname.ObjC_Cpp.get_class_name cpp_pname)
(Typ.Procname.get_method pname)
in
if QuandaryConfig.is_endpoint qualified_pname then
taint_all_but_this_and_return ~make_source:(fun name desc ->
UserControlledEndpoint (name, desc) )
else if overrides_service_method pname tenv then
taint_all_but_this_and_return ~make_source:(fun name desc -> Endpoint (name, desc))
else Source.all_formals_untainted pdesc
| _ ->
Source.all_formals_untainted pdesc
let pp fmt = function
| Endpoint (formal_name, _) ->
F.fprintf fmt "Endpoint(%s)" (Mangled.to_string formal_name)
| EnvironmentVariable ->
F.pp_print_string fmt "EnvironmentVariable"
| ReadFile ->
F.pp_print_string fmt "File"
| CommandLineFlag (var, _) ->
F.fprintf fmt "CommandLineFlag(%a)" Var.pp var
| Other ->
F.pp_print_string fmt "Other"
| UserControlledEndpoint (formal_name, _) ->
F.fprintf fmt "UserControlledEndpoint(%s)" (Mangled.to_string formal_name)
end
module CppSource = Source.Make (SourceKind)
module SinkKind = struct
type t =
| BufferAccess (** read/write an array *)
| CreateFile (** create/open a file *)
| EnvironmentChange (** change environment variable or gflag *)
| HeapAllocation (** heap memory allocation *)
| ShellExec (** shell exec function *)
| SQLInjection (** unescaped query to a SQL database (could be read or write) *)
| SQLRead (** escaped read to a SQL database *)
| SQLWrite (** escaped write to a SQL database *)
| StackAllocation (** stack memory allocation *)
| URL (** URL creation *)
| Other (** for testing or uncategorized sinks *)
[@@deriving compare]
let matches ~caller ~callee = Int.equal 0 (compare caller callee)
let of_string = function
| "BufferAccess" ->
BufferAccess
| "CreateFile" ->
CreateFile
| "EnvironmentChange" ->
EnvironmentChange
| "HeapAllocation" ->
HeapAllocation
| "ShellExec" ->
ShellExec
| "SQLInjection" ->
SQLInjection
| "SQLRead" ->
SQLRead
| "SQLWrite" ->
SQLWrite
| "StackAllocation" ->
StackAllocation
| "URL" ->
URL
| _ ->
Other
let external_sinks =
List.filter_map
~f:(fun {QuandaryConfig.Sink.procedure; kinds; index} ->
parse_clang_procedure procedure kinds index )
(QuandaryConfig.Sink.of_json Config.quandary_sinks)
(* taint the nth parameter (0-indexed) *)
let taint_nth n kinds actuals =
if n < List.length actuals then
let indexes = IntSet.singleton n in
List.rev_map kinds ~f:(fun kind -> (kind, indexes))
else []
(* taint all parameters after the nth (exclusive) *)
let taint_after_nth n kinds actuals =
match
List.filter_mapi ~f:(fun actual_num _ -> Option.some_if (actual_num > n) actual_num) actuals
with
| [] ->
[]
| to_taint ->
let indexes = IntSet.of_list to_taint in
List.rev_map kinds ~f:(fun kind -> (kind, indexes))
let taint_all kinds actuals =
let indexes = IntSet.of_list (List.mapi ~f:(fun actual_num _ -> actual_num) actuals) in
List.rev_map kinds ~f:(fun kind -> (kind, indexes))
(* return Some(sink kinds) if [procedure_name] is in the list of externally specified sinks *)
let get_external_sink pname actuals =
let qualified_pname = Typ.Procname.get_qualifiers pname in
List.find_map
~f:(fun (qualifiers, kinds, index) ->
if QualifiedCppName.Match.match_qualifiers qualifiers qualified_pname then
let kinds = List.rev_map ~f:of_string kinds in
try
let n = int_of_string index in
let taint = taint_nth n kinds actuals in
Option.some_if (not (List.is_empty taint)) taint
with Failure _ ->
(* couldn't parse the index, just taint everything *)
Some (taint_all kinds actuals)
else None )
external_sinks
|> Option.value ~default:[]
let get pname actuals _ _ =
let is_buffer_like pname =
(* assume it's a buffer class if it's "vector-y", "array-y", or "string-y". don't want to
report on accesses to maps etc., but also want to recognize custom vectors like fbvector
rather than overfitting to std::vector *)
let typename =
Typ.Procname.get_qualifiers pname |> QualifiedCppName.strip_template_args
|> QualifiedCppName.to_qual_string |> String.lowercase
in
String.is_substring ~substring:"vec" typename
|| String.is_substring ~substring:"array" typename
|| String.is_substring ~substring:"string" typename
in
match pname with
| Typ.Procname.ObjC_Cpp cpp_name -> (
match
( QualifiedCppName.to_list
(Typ.Name.unqualified_name (Typ.Procname.ObjC_Cpp.get_class_type_name cpp_name))
, Typ.Procname.get_method pname )
with
| ( ["std"; ("basic_fstream" | "basic_ifstream" | "basic_ofstream")]
, ("basic_fstream" | "basic_ifstream" | "basic_ofstream" | "open") ) ->
taint_nth 1 [CreateFile] actuals
| _, "operator[]" when Config.developer_mode && is_buffer_like pname ->
taint_nth 1 [BufferAccess] actuals
| _ ->
get_external_sink pname actuals )
| Typ.Procname.C _
when String.is_substring ~substring:"SetCommandLineOption" (Typ.Procname.to_string pname) ->
taint_nth 1 [EnvironmentChange] actuals
| Typ.Procname.C _
when Config.developer_mode && Typ.Procname.equal pname BuiltinDecl.__array_access ->
taint_all [BufferAccess] actuals
| Typ.Procname.C _ when Typ.Procname.equal pname BuiltinDecl.__set_array_length ->
(* called when creating a stack-allocated array *)
taint_nth 1 [StackAllocation] actuals
| Typ.Procname.C _ -> (
match Typ.Procname.to_string pname with
| "creat" | "fopen" | "freopen" | "open" ->
taint_nth 0 [CreateFile] actuals
| "curl_easy_setopt" -> (
(* magic constant for setting request URL *)
let controls_request = function
| 10002 (* CURLOPT_URL *) | 10015 (* CURLOPT_POSTFIELDS *) ->
true
| _ ->
false
in
(* first two actuals are curl object + integer code for data kind. *)
match List.nth actuals 1 with
| Some exp -> (
match HilExp.eval exp with
| Some (Const.Cint i) ->
(* check if the data kind might be CURLOPT_URL *)
IntLit.to_int i
|> Option.value_map ~default:[] ~f:(fun n ->
if controls_request n then taint_after_nth 1 [URL] actuals else [] )
| _ ->
(* can't statically resolve data kind; taint it just in case *)
taint_after_nth 1 [URL] actuals )
| None ->
[] )
| "execl" | "execlp" | "execle" | "execv" | "execve" | "execvp" | "system" ->
taint_all [ShellExec] actuals
| "openat" ->
taint_nth 1 [CreateFile] actuals
| "popen" ->
taint_nth 0 [ShellExec] actuals
| "putenv" ->
taint_nth 0 [EnvironmentChange] actuals
| ("brk" | "calloc" | "malloc" | "realloc" | "sbrk") when Config.developer_mode ->
taint_all [HeapAllocation] actuals
| "rename" ->
taint_all [CreateFile] actuals
| "strcpy" when Config.developer_mode ->
(* warn if source array is tainted *)
taint_nth 1 [BufferAccess] actuals
| ("memcpy" | "memmove" | "memset" | "strncpy" | "wmemcpy" | "wmemmove")
when Config.developer_mode ->
(* warn if count argument is tainted *)
taint_nth 2 [BufferAccess] actuals
| _ ->
get_external_sink pname actuals )
| Typ.Procname.Block _ ->
[]
| pname ->
L.(die InternalError) "Non-C++ procname %a in C++ analysis" Typ.Procname.pp pname
let pp fmt kind =
F.pp_print_string fmt
( match kind with
| BufferAccess ->
"BufferAccess"
| CreateFile ->
"CreateFile"
| EnvironmentChange ->
"EnvironmentChange"
| HeapAllocation ->
"HeapAllocation"
| ShellExec ->
"ShellExec"
| SQLInjection ->
"SQLInjection"
| SQLRead ->
"SQLRead"
| SQLWrite ->
"SQLWrite"
| StackAllocation ->
"StackAllocation"
| URL ->
"URL"
| Other ->
"Other" )
end
module CppSink = Sink.Make (SinkKind)
module CppSanitizer = struct
type t =
| EscapeShell (** escape string to sanitize shell commands *)
| EscapeSQL (** escape string to sanitize SQL queries *)
| EscapeURL (** escape string to sanitize URLs (e.g., prevent injecting GET/POST params) *)
| All (** sanitizes all forms of taint *)
[@@deriving compare]
let equal = [%compare.equal: t]
let of_string = function
| "EscapeShell" ->
EscapeShell
| "EscapeSQL" ->
EscapeSQL
| "EscapeURL" ->
EscapeURL
| _ ->
All
let external_sanitizers =
List.map
~f:(fun {QuandaryConfig.Sanitizer.procedure; kind} ->
(QualifiedCppName.Match.of_fuzzy_qual_names [procedure], of_string kind) )
(QuandaryConfig.Sanitizer.of_json Config.quandary_sanitizers)
let get pname _tenv =
let qualified_pname = Typ.Procname.get_qualifiers pname in
List.find_map
~f:(fun (qualifiers, kind) ->
if QualifiedCppName.Match.match_qualifiers qualifiers qualified_pname then Some kind
else None )
external_sanitizers
let pp fmt = function
| EscapeShell ->
F.pp_print_string fmt "EscapeShell"
| EscapeSQL ->
F.pp_print_string fmt "EscapeSQL"
| EscapeURL ->
F.pp_print_string fmt "EscapeURL"
| All ->
F.pp_print_string fmt "All"
end
include Trace.Make (struct
module Source = CppSource
module Sink = CppSink
module Sanitizer = CppSanitizer
(* return true if code injection is possible because the source is a string/is not sanitized with
[escape_sanitizer] *)
let is_injection_possible ?typ escape_sanitizer sanitizers =
let is_escaped = List.mem sanitizers escape_sanitizer ~equal:Sanitizer.equal in
(not is_escaped)
&&
match typ with
| Some (Typ.Tint _ | Tfloat _ | Tvoid) ->
false
| _ ->
(* possible a string/object/struct type; assume injection possible *)
true
let get_report source sink sanitizers =
match (Source.kind source, Sink.kind sink) with
| _ when List.mem sanitizers Sanitizer.All ~equal:Sanitizer.equal ->
(* the All sanitizer clears any form of taint; don't report *)
None
| (Endpoint (_, typ) | UserControlledEndpoint (_, typ)), CreateFile ->
Option.some_if
(is_injection_possible ~typ Sanitizer.EscapeShell sanitizers)
IssueType.untrusted_file_risk
| (Endpoint (_, typ) | UserControlledEndpoint (_, typ)), URL ->
Option.some_if
(is_injection_possible ~typ Sanitizer.EscapeURL sanitizers)
IssueType.untrusted_url_risk
| ( (CommandLineFlag (_, typ) | Endpoint (_, typ) | UserControlledEndpoint (_, typ))
, SQLInjection ) ->
if is_injection_possible ~typ Sanitizer.EscapeSQL sanitizers then
(* SQL injection if the caller of the endpoint doesn't sanitize on its end *)
Some IssueType.sql_injection_risk
else
(* no injection risk, but still user-controlled *)
Some IssueType.user_controlled_sql_risk
| (Endpoint _ | UserControlledEndpoint _), (SQLRead | SQLWrite) ->
(* no injection risk, but still user-controlled *)
Some IssueType.user_controlled_sql_risk
| (Endpoint _ | UserControlledEndpoint _), EnvironmentChange ->
(* user-controlled environment mutation *)
Some IssueType.untrusted_environment_change_risk
| (CommandLineFlag (_, typ) | Endpoint (_, typ) | UserControlledEndpoint (_, typ)), ShellExec
->
(* code injection if the caller of the endpoint doesn't sanitize on its end *)
Option.some_if
(is_injection_possible ~typ Sanitizer.EscapeShell sanitizers)
IssueType.shell_injection_risk
| ( ( UserControlledEndpoint _
| Endpoint _
| CommandLineFlag _
| EnvironmentVariable
| ReadFile
| Other )
, BufferAccess ) ->
(* untrusted data of any kind flowing to buffer *)
Some IssueType.untrusted_buffer_access
| (EnvironmentVariable | ReadFile | Other), ShellExec ->
(* environment var, or file data flowing to shell *)
Option.some_if
(is_injection_possible Sanitizer.EscapeShell sanitizers)
IssueType.shell_injection
| (EnvironmentVariable | ReadFile | Other), SQLInjection ->
(* untrusted flag, environment var, or file data flowing to SQL *)
Option.some_if
(is_injection_possible Sanitizer.EscapeSQL sanitizers)
IssueType.sql_injection
| Other, URL ->
(* untrusted flag, environment var, or file data flowing to URL *)
Option.some_if
(is_injection_possible Sanitizer.EscapeURL sanitizers)
IssueType.untrusted_url_risk
| ( ( CommandLineFlag _
| Endpoint _
| UserControlledEndpoint _
| EnvironmentVariable
| ReadFile
| Other )
, HeapAllocation ) ->
(* untrusted data of any kind flowing to heap allocation. this can cause crashes or DOS. *)
Some IssueType.untrusted_heap_allocation
| ( ( CommandLineFlag _
| Endpoint _
| UserControlledEndpoint _
| EnvironmentVariable
| ReadFile
| Other )
, StackAllocation ) ->
(* untrusted data of any kind flowing to stack buffer allocation. trying to allocate a stack
buffer that's too large will cause a stack overflow. *)
Some IssueType.untrusted_variable_length_array
| ( (CommandLineFlag _ | EnvironmentVariable | ReadFile)
, (CreateFile | EnvironmentChange | SQLRead | SQLWrite | URL) ) ->
None
| Other, _ ->
(* Other matches everything *)
Some IssueType.quandary_taint_error
| _, Other ->
Some IssueType.quandary_taint_error
end)