source/interprocedural_analyses/taint/model.ml (557 lines of code) (raw):
(*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
open Core
open Pyre
open Analysis
open Interprocedural
open Domains
let json_to_string ~indent json =
let lines = Yojson.Safe.to_string json |> Yojson.Safe.prettify |> String.split ~on:'\n' in
match lines with
| [line] -> line
| lines ->
lines
|> List.map ~f:(fun line -> indent ^ line)
|> String.concat ~sep:"\n"
|> fun content -> "\n" ^ content
module Forward = struct
type t = { source_taint: ForwardState.t }
let pp formatter { source_taint } =
Format.fprintf
formatter
" Sources: %s"
(json_to_string ~indent:" " (ForwardState.to_json source_taint))
let show = Format.asprintf "%a" pp
let empty = { source_taint = ForwardState.empty }
let is_empty { source_taint } = ForwardState.is_empty source_taint
let obscure = empty
let join { source_taint = left } { source_taint = right } =
{ source_taint = ForwardState.join left right }
let widen ~iteration ~previous:{ source_taint = prev } ~next:{ source_taint = next } =
{ source_taint = ForwardState.widen ~iteration ~prev ~next }
let reached_fixpoint
~iteration:_
~previous:{ source_taint = previous }
~next:{ source_taint = next }
=
ForwardState.less_or_equal ~left:next ~right:previous
end
module Backward = struct
type t = {
taint_in_taint_out: BackwardState.t;
sink_taint: BackwardState.t;
}
let pp formatter { taint_in_taint_out; sink_taint } =
Format.fprintf
formatter
" Taint-in-taint-out: %s\n Sinks: %s"
(json_to_string ~indent:" " (BackwardState.to_json taint_in_taint_out))
(json_to_string ~indent:" " (BackwardState.to_json sink_taint))
let show = Format.asprintf "%a" pp
let empty = { sink_taint = BackwardState.empty; taint_in_taint_out = BackwardState.empty }
let is_empty { sink_taint; taint_in_taint_out } =
BackwardState.is_empty sink_taint && BackwardState.is_empty taint_in_taint_out
let obscure = empty
let join
{ sink_taint = sink_taint_left; taint_in_taint_out = tito_left }
{ sink_taint = sink_taint_right; taint_in_taint_out = tito_right }
=
{
sink_taint = BackwardState.join sink_taint_left sink_taint_right;
taint_in_taint_out = BackwardState.join tito_left tito_right;
}
let widen
~iteration
~previous:{ sink_taint = sink_taint_previous; taint_in_taint_out = tito_previous }
~next:{ sink_taint = sink_taint_next; taint_in_taint_out = tito_next }
=
let sink_taint =
BackwardState.widen ~iteration ~prev:sink_taint_previous ~next:sink_taint_next
in
let taint_in_taint_out = BackwardState.widen ~iteration ~prev:tito_previous ~next:tito_next in
{ sink_taint; taint_in_taint_out }
let reached_fixpoint
~iteration:_
~previous:{ sink_taint = sink_taint_previous; taint_in_taint_out = tito_previous }
~next:{ sink_taint = sink_taint_next; taint_in_taint_out = tito_next }
=
BackwardState.less_or_equal ~left:sink_taint_next ~right:sink_taint_previous
&& BackwardState.less_or_equal ~left:tito_next ~right:tito_previous
end
module Sanitizers = struct
type t = {
(* `global` applies to all parameters and the return value.
* For callables:
* - sources are only sanitized in the forward trace;
* - sinks are only sanitized in the backward trace;
* - titos are sanitized in both traces (using sanitize taint transforms).
* For attribute models, sanitizers are applied on attribute accesses, in both traces.
*)
global: Sanitize.t;
(* Sanitizers applying to all parameters, in both traces. *)
parameters: Sanitize.t;
(* Map from parameter or return value to sanitizers applying in both traces. *)
roots: SanitizeRootMap.t;
}
let pp formatter { global; parameters; roots } =
Format.fprintf
formatter
" Global Sanitizer: %s\n Parameters Sanitizer: %s\n Sanitizers: %s"
(json_to_string ~indent:" " (Sanitize.to_json global))
(json_to_string ~indent:" " (Sanitize.to_json parameters))
(json_to_string ~indent:" " (SanitizeRootMap.to_json roots))
let show = Format.asprintf "%a" pp
let empty =
{ global = Sanitize.empty; parameters = Sanitize.empty; roots = SanitizeRootMap.bottom }
let is_empty { global; parameters; roots } =
Sanitize.is_empty global && Sanitize.is_empty parameters && SanitizeRootMap.is_bottom roots
let join
{ global = global_left; parameters = parameters_left; roots = roots_left }
{ global = global_right; parameters = parameters_right; roots = roots_right }
=
{
global = Sanitize.join global_left global_right;
parameters = Sanitize.join parameters_left parameters_right;
roots = SanitizeRootMap.join roots_left roots_right;
}
let widen ~iteration:_ ~previous ~next = join previous next
end
module Mode = struct
let name = "modes"
type t =
| Obscure
| SkipAnalysis (* Don't analyze at all *)
| SkipDecoratorWhenInlining
| SkipOverrides
[@@deriving compare]
let pp formatter = function
| Obscure -> Format.fprintf formatter "Obscure"
| SkipAnalysis -> Format.fprintf formatter "SkipAnalysis"
| SkipDecoratorWhenInlining -> Format.fprintf formatter "SkipDecoratorWhenInlining"
| SkipOverrides -> Format.fprintf formatter "SkipOverrides"
let show = Format.asprintf "%a" pp
let to_json mode = `String (show mode)
end
module ModeSet = struct
module T = Abstract.SetDomain.Make (Mode)
include T
let empty = T.bottom
let is_empty = T.is_bottom
let equal left right = T.less_or_equal ~left ~right && T.less_or_equal ~left:right ~right:left
let to_json modes = `List (modes |> T.elements |> List.map ~f:Mode.to_json)
let pp formatter modes =
Format.fprintf formatter " Modes: %s" (json_to_string ~indent:" " (to_json modes))
end
type t = {
forward: Forward.t;
backward: Backward.t;
sanitizers: Sanitizers.t;
modes: ModeSet.t;
}
let pp formatter { forward; backward; sanitizers; modes } =
Format.fprintf
formatter
"%a\n%a\n%a\n%a"
Forward.pp
forward
Backward.pp
backward
Sanitizers.pp
sanitizers
ModeSet.pp
modes
let show = Format.asprintf "%a" pp
let is_empty ~with_modes { forward; backward; sanitizers; modes } =
Forward.is_empty forward
&& Backward.is_empty backward
&& Sanitizers.is_empty sanitizers
&& ModeSet.equal with_modes modes
let empty_model =
{
forward = Forward.empty;
backward = Backward.empty;
sanitizers = Sanitizers.empty;
modes = ModeSet.empty;
}
let empty_skip_model =
{
forward = Forward.empty;
backward = Backward.empty;
sanitizers = Sanitizers.empty;
modes = ModeSet.singleton SkipAnalysis;
}
let obscure_model =
{
forward = Forward.obscure;
backward = Backward.obscure;
sanitizers = Sanitizers.empty;
modes = ModeSet.singleton Obscure;
}
let is_obscure { modes; _ } = ModeSet.contains Obscure modes
let remove_obscureness ({ modes; _ } as model) = { model with modes = ModeSet.remove Obscure modes }
let remove_sinks model =
{ model with backward = { model.backward with sink_taint = BackwardState.empty } }
let add_obscure_sink ~resolution ~call_target model =
match Target.get_callable_t call_target with
| None -> model
| Some real_target -> (
match
Target.get_module_and_definition
~resolution:(Resolution.global_resolution resolution)
real_target
with
| None ->
let () = Log.warning "Found no definition for %s" (Target.show call_target) in
model
| Some (_, { value = { signature = { parameters; _ }; _ }; _ }) ->
let open Domains in
let sink =
BackwardTaint.singleton (Sinks.NamedSink "Obscure") Frame.initial
|> BackwardState.Tree.create_leaf
in
let parameters = AccessPath.Root.normalize_parameters parameters in
let add_parameter_sink sink_taint (root, _, _) =
BackwardState.assign ~root ~path:[] sink sink_taint
in
let sink_taint =
List.fold_left ~init:model.backward.sink_taint ~f:add_parameter_sink parameters
in
{ model with backward = { model.backward with sink_taint } })
let join left right =
{
forward = Forward.join left.forward right.forward;
backward = Backward.join left.backward right.backward;
sanitizers = Sanitizers.join left.sanitizers right.sanitizers;
modes = ModeSet.join left.modes right.modes;
}
let widen ~iteration ~previous ~next =
{
forward = Forward.widen ~iteration ~previous:previous.forward ~next:next.forward;
backward = Backward.widen ~iteration ~previous:previous.backward ~next:next.backward;
sanitizers = Sanitizers.widen ~iteration ~previous:previous.sanitizers ~next:next.sanitizers;
modes = ModeSet.widen ~iteration ~prev:previous.modes ~next:next.modes;
}
let reached_fixpoint ~iteration ~previous ~next =
Forward.reached_fixpoint ~iteration ~previous:previous.forward ~next:next.forward
&& Backward.reached_fixpoint ~iteration ~previous:previous.backward ~next:next.backward
let strip_for_callsite
{ forward = { source_taint }; backward = { sink_taint; taint_in_taint_out }; sanitizers; modes }
=
(* Remove positions and other info that are not needed at call site *)
let source_taint =
source_taint
|> ForwardState.transform Features.TitoPositionSet.Self Map ~f:(fun _ ->
Features.TitoPositionSet.bottom)
|> ForwardState.transform ForwardTaint.call_info Map ~f:CallInfo.strip_for_callsite
in
let sink_taint =
sink_taint
|> BackwardState.transform Features.TitoPositionSet.Self Map ~f:(fun _ ->
Features.TitoPositionSet.bottom)
|> BackwardState.transform BackwardTaint.call_info Map ~f:CallInfo.strip_for_callsite
in
let taint_in_taint_out =
taint_in_taint_out
|> BackwardState.transform Features.TitoPositionSet.Self Map ~f:(fun _ ->
Features.TitoPositionSet.bottom)
|> BackwardState.transform BackwardTaint.call_info Map ~f:CallInfo.strip_for_callsite
in
{ forward = { source_taint }; backward = { sink_taint; taint_in_taint_out }; sanitizers; modes }
let apply_sanitizers
{
forward = { source_taint };
backward = { taint_in_taint_out; sink_taint };
sanitizers = { global; parameters; roots } as sanitizers;
modes;
}
=
let kinds_to_sanitize_transforms ~sources ~sinks =
let source_transforms = Sources.Set.to_sanitize_transforms_exn sources in
let sink_transforms = Sinks.Set.to_sanitize_transforms_exn sinks in
SanitizeTransform.Set.union source_transforms sink_transforms
in
let sanitize_tito ?(sources = Sources.Set.empty) ?(sinks = Sinks.Set.empty) taint_in_taint_out =
let transforms = kinds_to_sanitize_transforms ~sources ~sinks in
BackwardState.apply_sanitize_transforms transforms taint_in_taint_out
in
let sanitize_tito_parameter
parameter
?(sources = Sources.Set.empty)
?(sinks = Sinks.Set.empty)
taint_in_taint_out
=
let sanitize_tito_taint_tree = function
| None -> BackwardState.Tree.bottom
| Some taint_tree ->
let transforms = kinds_to_sanitize_transforms ~sources ~sinks in
BackwardState.Tree.apply_sanitize_transforms transforms taint_tree
in
BackwardState.update taint_in_taint_out parameter ~f:sanitize_tito_taint_tree
in
(* Apply the global sanitizer. *)
(* Here, we are applying the legacy behavior of sanitizers, where we only
* sanitize the forward trace or the backward trace. *)
let source_taint =
(* @Sanitize(TaintSource[...]) *)
match global.sources with
| Some All -> ForwardState.empty
| Some (Specific sanitized_sources) -> ForwardState.sanitize sanitized_sources source_taint
| None -> source_taint
in
let taint_in_taint_out =
(* @Sanitize(TaintInTaintOut[...]) *)
match global.tito with
| Some All -> BackwardState.empty
| Some (Specific { sanitized_tito_sources; sanitized_tito_sinks }) ->
sanitize_tito ~sources:sanitized_tito_sources ~sinks:sanitized_tito_sinks taint_in_taint_out
| None -> taint_in_taint_out
in
let sink_taint =
(* @Sanitize(TaintSink[...]) *)
match global.sinks with
| Some All -> BackwardState.empty
| Some (Specific sanitized_sinks) -> BackwardState.sanitize sanitized_sinks sink_taint
| None -> sink_taint
in
(* Apply the parameters sanitizer. *)
(* Here, we apply sanitizers both in the forward and backward trace. *)
(* Note that by design, sanitizing a specific source or sink also sanitizes
* taint-in-taint-out for that source/sink. *)
let sink_taint, taint_in_taint_out =
(* Sanitize(Parameters[TaintSource[...]]) *)
match parameters.sources with
| Some All -> sink_taint, taint_in_taint_out
| Some (Specific sanitized_sources) ->
let sanitized_sources_transforms =
Sources.Set.to_sanitize_transforms_exn sanitized_sources
in
let sink_taint =
sink_taint
|> BackwardState.apply_sanitize_transforms sanitized_sources_transforms
|> BackwardState.transform BackwardTaint.kind Filter ~f:Issue.sink_can_match_rule
in
let taint_in_taint_out = sanitize_tito ~sources:sanitized_sources taint_in_taint_out in
sink_taint, taint_in_taint_out
| None -> sink_taint, taint_in_taint_out
in
let taint_in_taint_out =
(* Sanitize(Parameters[TaintInTaintOut[...]]) *)
match parameters.tito with
| Some All -> BackwardState.empty
| Some (Specific { sanitized_tito_sources; sanitized_tito_sinks }) ->
sanitize_tito ~sources:sanitized_tito_sources ~sinks:sanitized_tito_sinks taint_in_taint_out
| _ -> taint_in_taint_out
in
let sink_taint, taint_in_taint_out =
(* Sanitize(Parameters[TaintSink[...]]) *)
match parameters.sinks with
| Some All ->
let sink_taint = BackwardState.empty in
sink_taint, taint_in_taint_out
| Some (Specific sanitized_sinks) ->
let sink_taint = BackwardState.sanitize sanitized_sinks sink_taint in
let taint_in_taint_out = sanitize_tito ~sinks:sanitized_sinks taint_in_taint_out in
sink_taint, taint_in_taint_out
| None -> sink_taint, taint_in_taint_out
in
(* Apply the return sanitizer. *)
let sanitize_return sanitize (source_taint, taint_in_taint_out, sink_taint) =
let root = AccessPath.Root.LocalResult in
let source_taint, taint_in_taint_out =
(* def foo() -> Sanitize[TaintSource[...]] *)
match sanitize.Sanitize.sources with
| Some All ->
let source_taint = ForwardState.remove root source_taint in
source_taint, taint_in_taint_out
| Some (Specific sanitized_sources) ->
let filter_sources = function
| None -> ForwardState.Tree.bottom
| Some taint_tree -> ForwardState.Tree.sanitize sanitized_sources taint_tree
in
let source_taint = ForwardState.update source_taint root ~f:filter_sources in
let taint_in_taint_out = sanitize_tito ~sources:sanitized_sources taint_in_taint_out in
source_taint, taint_in_taint_out
| None -> source_taint, taint_in_taint_out
in
let taint_in_taint_out =
(* def foo() -> Sanitize[TaintInTaintOut[...]] *)
match sanitize.Sanitize.tito with
| Some All -> BackwardState.remove root taint_in_taint_out
| Some (Specific { sanitized_tito_sources; sanitized_tito_sinks }) ->
sanitize_tito
~sources:sanitized_tito_sources
~sinks:sanitized_tito_sinks
taint_in_taint_out
| _ -> taint_in_taint_out
in
let source_taint, taint_in_taint_out =
(* def foo() -> Sanitize[TaintSink[...]] *)
match sanitize.Sanitize.sinks with
| Some All -> source_taint, taint_in_taint_out
| Some (Specific sanitized_sinks) ->
let sanitized_sinks_transforms = Sinks.Set.to_sanitize_transforms_exn sanitized_sinks in
let source_taint =
source_taint
|> ForwardState.apply_sanitize_transforms sanitized_sinks_transforms
|> ForwardState.transform ForwardTaint.kind Filter ~f:Issue.source_can_match_rule
in
let taint_in_taint_out = sanitize_tito ~sinks:sanitized_sinks taint_in_taint_out in
source_taint, taint_in_taint_out
| None -> source_taint, taint_in_taint_out
in
source_taint, taint_in_taint_out, sink_taint
in
(* Apply the parameter-specific sanitizers. *)
let sanitize_parameter (parameter, sanitize) (source_taint, taint_in_taint_out, sink_taint) =
let sink_taint, taint_in_taint_out =
(* def foo(x: Sanitize[TaintSource[...]]): ... *)
match sanitize.Sanitize.sources with
| Some All -> sink_taint, taint_in_taint_out
| Some (Specific sanitized_sources) ->
let apply_taint_transforms = function
| None -> BackwardState.Tree.bottom
| Some taint_tree ->
let sanitized_sources_transforms =
Sources.Set.to_sanitize_transforms_exn sanitized_sources
in
taint_tree
|> BackwardState.Tree.apply_sanitize_transforms sanitized_sources_transforms
|> BackwardState.Tree.transform
BackwardTaint.kind
Filter
~f:Issue.sink_can_match_rule
in
let sink_taint = BackwardState.update sink_taint parameter ~f:apply_taint_transforms in
let taint_in_taint_out =
sanitize_tito_parameter parameter ~sources:sanitized_sources taint_in_taint_out
in
sink_taint, taint_in_taint_out
| None -> sink_taint, taint_in_taint_out
in
let taint_in_taint_out =
(* def foo(x: Sanitize[TaintInTaintOut[...]]): ... *)
match sanitize.Sanitize.tito with
| Some All -> BackwardState.remove parameter taint_in_taint_out
| Some (Specific { sanitized_tito_sources; sanitized_tito_sinks }) ->
sanitize_tito_parameter
parameter
~sources:sanitized_tito_sources
~sinks:sanitized_tito_sinks
taint_in_taint_out
| None -> taint_in_taint_out
in
let sink_taint, taint_in_taint_out =
(* def foo(x: Sanitize[TaintSink[...]]): ... *)
match sanitize.Sanitize.sinks with
| Some All ->
let sink_taint = BackwardState.remove parameter sink_taint in
sink_taint, taint_in_taint_out
| Some (Specific sanitized_sinks) ->
let filter_sinks = function
| None -> BackwardState.Tree.bottom
| Some taint_tree -> BackwardState.Tree.sanitize sanitized_sinks taint_tree
in
let sink_taint = BackwardState.update sink_taint parameter ~f:filter_sinks in
let taint_in_taint_out =
sanitize_tito_parameter parameter ~sinks:sanitized_sinks taint_in_taint_out
in
sink_taint, taint_in_taint_out
| None -> sink_taint, taint_in_taint_out
in
source_taint, taint_in_taint_out, sink_taint
in
let sanitize_root (root, sanitize) (source_taint, taint_in_taint_out, sink_taint) =
match root with
| AccessPath.Root.LocalResult ->
sanitize_return sanitize (source_taint, taint_in_taint_out, sink_taint)
| PositionalParameter _
| NamedParameter _
| StarParameter _
| StarStarParameter _ ->
sanitize_parameter (root, sanitize) (source_taint, taint_in_taint_out, sink_taint)
| Variable _ -> failwith "unexpected"
in
let source_taint, taint_in_taint_out, sink_taint =
SanitizeRootMap.fold
SanitizeRootMap.KeyValue
~f:sanitize_root
~init:(source_taint, taint_in_taint_out, sink_taint)
roots
in
{ forward = { source_taint }; backward = { sink_taint; taint_in_taint_out }; sanitizers; modes }
let should_externalize { forward; backward; sanitizers; _ } =
(not (Forward.is_empty forward))
|| (not (Backward.is_empty backward))
|| not (Sanitizers.is_empty sanitizers)
let to_json
~filename_lookup
callable
{
forward = { source_taint };
backward = { sink_taint; taint_in_taint_out };
sanitizers =
{ global = global_sanitizer; parameters = parameters_sanitizer; roots = root_sanitizers };
modes;
}
=
let callable_name = Interprocedural.Target.external_target_name callable in
let model_json = ["callable", `String callable_name] in
let model_json =
if not (ForwardState.is_empty source_taint) then
model_json @ ["sources", ForwardState.to_external_json ~filename_lookup source_taint]
else
model_json
in
let model_json =
if not (BackwardState.is_empty sink_taint) then
model_json @ ["sinks", BackwardState.to_external_json ~filename_lookup sink_taint]
else
model_json
in
let model_json =
if not (BackwardState.is_empty taint_in_taint_out) then
model_json @ ["tito", BackwardState.to_external_json ~filename_lookup taint_in_taint_out]
else
model_json
in
let model_json =
if not (Sanitize.is_empty global_sanitizer) then
model_json @ ["global_sanitizer", Sanitize.to_json global_sanitizer]
else
model_json
in
let model_json =
if not (Sanitize.is_empty parameters_sanitizer) then
model_json @ ["parameters_sanitizer", Sanitize.to_json parameters_sanitizer]
else
model_json
in
let model_json =
if not (SanitizeRootMap.is_bottom root_sanitizers) then
model_json @ ["sanitizers", SanitizeRootMap.to_json root_sanitizers]
else
model_json
in
let model_json =
if not (ModeSet.is_empty modes) then
model_json @ ["modes", ModeSet.to_json modes]
else
model_json
in
`Assoc ["kind", `String "model"; "data", `Assoc model_json]
module WithTarget = struct
type nonrec t = {
model: t;
target: Target.t;
}
end
module WithCallTarget = struct
type nonrec t = {
model: t;
call_target: CallGraph.CallTarget.t;
}
end