diff --git a/CHANGELOG.md b/CHANGELOG.md index 016d0723c6..ff13b99863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [1.19.0](https://github.com/opengrep/opengrep/releases/tag/v1.19.0) - 09-04-2026 + +### Improvements + +* Elixir: fix taint propagation through for comprehensions and pipes by @dimitris-m in #650 +* Ruby: remove redundant Call wrapping in `expr_as_stmt` by @dimitris-m in #651 +* Fix `obj[key]` parsing in ruby by @corneliuhoffman in #649 + +**Full Changelog**: https://github.com/opengrep/opengrep/compare/v1.18.0...v1.19.0 + + +## [1.18.0](https://github.com/opengrep/opengrep/releases/tag/v1.18.0) - 07-04-2026 + +### Improvements + +* Elixir: Updates to syntax (part 1) by @maciejpirog in #642 +* Elixir: Updates to syntax (part 2) by @maciejpirog in #646 + +**Full Changelog**: https://github.com/opengrep/opengrep/compare/v1.17.0...v1.18.0 + + +## [1.17.0](https://github.com/opengrep/opengrep/releases/tag/v1.17.0) - 03-04-2026 + +### Improvements + +* Treat nested functions as lambdas by @dimitris-m in #621 +* Tainting: per-arity signature extraction for Clojure and Elixir by @dimitris-m in #582 +* Dockerfile: Preprocess for better line continuations and comments by @maciejpirog in #633 +* Elixir: fix map destructuring in function parameters by @dimitris-m in #637 +* Elixir: fix taint propagation through map value expressions by @dimitris-m in #640 +* Support chained method calls on constructor results with --taint-intrafile by @corneliuhoffman in #638 + +### Bug fixes + +* Fix(Ruby): zero argument method call syntax by @corneliuhoffman in #626 +* Fix: proper constructor taint analysis by @corneliuhoffman in #628 +* Fix: Elixir scanner CRLF handling by @dimitris-m in #639 + +### Refactoring + +* Refactoring of AST-to-IL, Step 1 by @maciejpirog in #622 + +### Developer tooling + +* Pretty-print IL in C-style syntax by @maciejpirog in #623 + +### Installation + +* Improve error reporting in install.sh by @dimitris-m in #620 + +### Security + +* [Aikido] AI Fix for Template Injection in GitHub Workflows Action by @aikido-autofix[bot] in #624 +* [Aikido] AI Fix for 3rd party Github Actions should be pinned by @aikido-autofix[bot] in #625 + +**Full Changelog**: https://github.com/opengrep/opengrep/compare/v1.16.5...v1.17.0 + + ## [1.16.5](https://github.com/opengrep/opengrep/releases/tag/v1.16.5) - 17-03-2026 ### Improvements diff --git a/cli/setup.py b/cli/setup.py index 89327e26ee..c298c33d32 100644 --- a/cli/setup.py +++ b/cli/setup.py @@ -124,7 +124,7 @@ def find_executable(env_name, exec_name): setuptools.setup( name="opengrep", - version="1.16.5", + version="1.19.0", author="Semgrep Inc., Opengrep", author_email="support@opengrep.dev", description="Lightweight static analysis for many languages. Find bug variants with patterns that look like source code.", diff --git a/cli/src/semgrep/__init__.py b/cli/src/semgrep/__init__.py index 5aa89ca810..35bfc71918 100644 --- a/cli/src/semgrep/__init__.py +++ b/cli/src/semgrep/__init__.py @@ -1,2 +1,2 @@ -__VERSION__ = "1.16.5" +__VERSION__ = "1.19.0" __SEMGREP_VERSION__ = "1.100.0" diff --git a/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml b/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml index 3089dc5beb..4b179b1615 100644 --- a/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml +++ b/languages/dockerfile/tree-sitter/Parse_dockerfile_tree_sitter.ml @@ -1214,12 +1214,147 @@ let ensure_trailing_newline str = | _ -> str ^ "\n" else str +(* Pre-process Dockerfile content before passing to tree-sitter. + + This fixes three classes of issues that the grammar alone can't handle: + + 1. Trailing whitespace after a line-continuation backslash (e.g. "\ " + at end of line). The grammar already accepts /\\[ \t]*\n/ as + line_continuation, but combine_sparse_toks in concat_shell_fragments + fills gaps with the fixed 2-byte string "\\\n". When the original gap + is wider (e.g. 4 bytes for "\ \n") the byte-offset arithmetic goes + off, which can surface as a Tok.NoTokenLocation crash. Stripping the + trailing whitespace makes every continuation token exactly "\\\n". + + 2. The "# escape=X" parser directive lets authors use an alternative + character (commonly the backtick on Windows/PowerShell Dockerfiles) as + the line-continuation marker. tree-sitter's grammar hard-codes the + backslash, so we replace each occurrence of X at the end of a line + with '\' before handing off to tree-sitter. + + 3. A comment line inside a multi-line continuation is transparent to the + continuation: Docker removes it before execution, so + + RUN echo a \ + # comment + b + + is equivalent to "RUN echo a b". tree-sitter's extras mechanism + already handles comments between tokens, but the OCaml post-processing + code (concat_shell_fragments) relies on the gap between shell-fragment + tokens to reconstruct positions. A comment line in that gap does not + carry a synthetic continuation, so the chain can break. We replace + comment lines with spaces of the same byte count and, when inside a + continuation, append a '\' at the last position so tree-sitter sees an + unbroken chain of line_continuation extras. + + Position safety: line_col_to_pos reads the *original* file and maps + (line, col) pairs to byte offsets. Our transforms only modify characters + at or after the last real token on each line (trailing whitespace, comment + text, the escape character itself). The (line, col) of every real token + is therefore unchanged between the original file and the preprocessed + content, so the position table remains valid. line_continuation tokens + live in the tree-sitter extras bucket; the OCaml callback discards them + via the `_extras` parameter and never calls conv on their positions, so + the synthetic '\' we insert on replaced comment lines is also harmless. +*) + +(* Matches "# escape=X" (case-insensitive, spaces optional around '='). + Group 1 captures the single non-whitespace escape character. *) +let escape_directive_re = + Pcre2_.regexp ~flags:[ `CASELESS ] {|^#\s*escape\s*=\s*(\S)|} + +(* If line looks like "# escape=X", return Some X, otherwise None. *) +let parse_escape_directive (line : string) : char option = + match Pcre2_.exec_noerr ~rex:escape_directive_re line with + | None -> None + | Some substrings -> Some (Pcre2.get_substring substrings 1).[0] + +(* Return the index of the last non-whitespace character in s, or -1. *) +let last_nws_index (s : string) : int = + let is_ws c = Char.equal c ' ' || Char.equal c '\t' in + let rec go i = if i < 0 || not (is_ws s.[i]) then i else go (i - 1) in + go (String.length s - 1) + +type preprocess_state = { + escape_char : char; + in_header : bool; + in_continuation : bool; +} + + (* Process one line (without its trailing '\n') and return the updated state + together with the transformed line. *) +let process_line (st : preprocess_state) (line : string) : preprocess_state * string = + let len = String.length line in + let lnws = last_nws_index line in + + if lnws < 0 then + (* Blank / all-whitespace line: pass through verbatim, leave header zone. *) + ({ st with in_header = false }, line) + else if Char.equal line.[0] '#' then begin + (* Comment line or parser directive. *) + match if st.in_header then parse_escape_directive line else None with + | Some c -> + (* Parser directive: record new escape char and keep line as-is. *) + ({ st with escape_char = c }, line) + | None -> + (* Regular comment: replace with spaces of the same byte count. + If we are mid-continuation, put a synthetic '\' at the end so + tree-sitter sees an unbroken continuation chain. + in_continuation is left unchanged: comment lines are transparent + to continuation state, just as Docker specifies. *) + let out = + if st.in_continuation && len >= 1 then + String.make (len - 1) ' ' ^ "\\" + else + String.make len ' ' + in + (st, out) + end else begin + (* Regular instruction line. *) + let bytes = Bytes.of_string line in + + (* Replace an alternative escape char at end of line with '\'. *) + if (not (Char.equal st.escape_char '\\')) + && Char.equal (Bytes.get bytes lnws) st.escape_char + then Bytes.set bytes lnws '\\'; + + (* Break a trailing "\\" pair so tree-sitter does not greedily consume it + as a shell_fragment before recognising the final '\' as + line_continuation. *) + if lnws >= 1 + && Char.equal (Bytes.get bytes lnws) '\\' + && Char.equal (Bytes.get bytes (lnws - 1)) '\\' + then Bytes.set bytes (lnws - 1) ' '; + + (* Strip trailing whitespace: emit only up to the last non-ws char so the + continuation token is exactly "\\\n", matching combine_sparse_toks. *) + let out = Bytes.sub_string bytes 0 (lnws + 1) in + let continues = Char.equal out.[lnws] '\\' in + ({ escape_char = st.escape_char; in_header = false; in_continuation = continues }, out) + end + +let preprocess_dockerfile (content : string) : string = + let init = { escape_char = '\\'; in_header = true; in_continuation = false } in + let lines = String.split_on_char '\n' content in + let _state, rev_lines = + List.fold_left + (fun (st, acc) line -> + let st', out = process_line st line in + (st', out :: acc)) + (init, []) + lines + in + String.concat "\n" (List.rev rev_lines) + let parse file = H.wrap_parser (fun () -> let contents = - UFile.read_file file |> normalize_line_endings + UFile.read_file file + |> normalize_line_endings |> ensure_trailing_newline + |> preprocess_dockerfile in Tree_sitter_dockerfile.Parse.string ~src_file:!!file contents) (fun cst _extras -> @@ -1236,7 +1371,10 @@ let parse_pattern str = let input_kind = AST_bash.Pattern in H.wrap_parser (fun () -> - str |> ensure_trailing_newline |> Tree_sitter_dockerfile.Parse.string) + str + |> ensure_trailing_newline + |> preprocess_dockerfile + |> Tree_sitter_dockerfile.Parse.string) (fun cst _extras -> let file = Fpath.v "" in let env = diff --git a/languages/elixir/ast/AST_elixir.ml b/languages/elixir/ast/AST_elixir.ml index af3d461f6c..951e421ad8 100644 --- a/languages/elixir/ast/AST_elixir.ml +++ b/languages/elixir/ast/AST_elixir.ml @@ -306,8 +306,19 @@ and stmt = * stmts * (tok * stmts) option * tok (* 'end' *) + | Try of tok (* 'try' *) * do_block + | Throw of tok (* 'throw' *) * expr + (* https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1 *) + | For of tok (* 'for' *) * for_clause list * stmts bracket (* do body *) | D of definition +(* ref: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1 + * A for_clause is either a generator (pattern <- collection) or a filter. + *) +and for_clause = + | ForGenerator of expr (* pattern *) * tok (* '<-' *) * expr (* collection *) + | ForFilter of expr + (* ------------------------------------------------------------------------- *) (* Definitions *) (* ------------------------------------------------------------------------- *) @@ -326,6 +337,9 @@ and function_definition = { f_guard : expr option; (* bracket is do/end *) f_body : stmts bracket; + (* rescue/catch/after/else clauses from the implicit-try form: + * def foo(x) do body rescue E -> handler end *) + f_rescue : (exn_clause_kind wrap * body_or_clauses) list; f_is_private : bool; } diff --git a/languages/elixir/ast/Elixir_to_elixir.ml b/languages/elixir/ast/Elixir_to_elixir.ml index e210355f90..bec8e17f50 100644 --- a/languages/elixir/ast/Elixir_to_elixir.ml +++ b/languages/elixir/ast/Elixir_to_elixir.ml @@ -28,7 +28,7 @@ open AST_elixir (*****************************************************************************) (* Visitor *) (*****************************************************************************) -let make_funcdef ~tdef ~ident ~params ~guard ~tdo ~body ~tend ~def_str = +let make_funcdef ~tdef ~ident ~params ~guard ~tdo ~body ~tend ~rescue ~def_str = let f_body = match tdo, tend with | Some tdo, Some tend -> (tdo, body, tend) @@ -41,11 +41,30 @@ let make_funcdef ~tdef ~ident ~params ~guard ~tdo ~body ~tend ~def_str = f_params = params; f_guard = guard; f_body; + f_rescue = rescue; f_is_private = String.equal def_str "defp"; } in S (D (FuncDef [def])) +(* In Elixir, we can skip the arg list in function definition if it is empty. + * We preprocess these cases to avoid code duplication in the visitor. *) +let normalize_function_header (x : call) : call = + match x with + | (I (Id (( "def" | "defp" ), _)) as a), + (l, ([ I ident ], kw), r), + c -> + a, + (l, ([ Call (I ident, Tok.unsafe_fake_bracket ([], []), None) ], kw), r), + c + | (I (Id (( "def" | "defp" ), _)) as a), + (l, ([ When (I ident, wtok, wguard) ], kw), r), + c -> + a, + (l, ([ When (Call (I ident, Tok.unsafe_fake_bracket ([], []), None), wtok, wguard) ], kw), r), + c + | _ -> x + class ['self] visitor = let params_of_args (args : arguments bracket) : parameters = let l, (exprs, kwdargs), r = args in @@ -53,7 +72,9 @@ class ['self] visitor = exprs |> List_.map (function | I id -> P { pname = id; pdefault = None } - (* TODO: recognize default value with \\ *) + (* In Elixir you can have a default value only for ident params (no pats) *) + | BinaryOp (I id, (ODefault, tok), d) -> + P { pname = id; pdefault = Some (tok, d) } | x -> OtherParamExpr x) in let ys = @@ -72,9 +93,20 @@ class ['self] visitor = object (self : 'self) inherit [_] map - + method private for_clauses env (args : expr list) : for_clause list = + List_.map (fun (arg : expr) -> + match arg with + | BinaryOp (pat, (OLeftArrow, tarrow), collection) -> + let pat = self#visit_expr env pat in + let collection = self#visit_expr env collection in + ForGenerator (pat, tarrow, collection) + | e -> + let e = self#visit_expr env e in + ForFilter e + ) args + method! visit_Call env (x : call) = - match x with + match normalize_function_header x with (* https://hexdocs.pm/elixir/Kernel.html#if/2 * TODO? recognize also the compact form 'if(cond, :do then)' ? *) @@ -92,15 +124,17 @@ class ['self] visitor = (* TODO? warning about unrecognized form? failwith ? *) Call (self#visit_call env x)) (* https://hexdocs.pm/elixir/Kernel.html#def/2 - * TODO: handle "implicit try" form + * The optional extras (rescue/catch/after/else) are the "implicit try" + * form; they are stored in f_rescue and translated to a Try block in + * Elixir_to_generic.ml. *) | ( I (Id (( "def" | "defp" ) as def_str, tdef)), (_, ([ Call (I ident, args, None) ], []), _), - Some (tdo, (Body body, []), tend) ) -> + Some (tdo, (Body body, extras), tend) ) -> let body = self#visit_body env body in let params = params_of_args args in make_funcdef ~tdef ~ident ~params ~guard:None ~tdo:(Some tdo) - ~body ~tend:(Some tend) ~def_str + ~body ~tend:(Some tend) ~rescue:extras ~def_str | ( I (Id (( "def" | "defp" ) as def_str, tdef)), (_, ([ Call (I ident, args, None) ], [ Kw_expr ((X1 (do_kw, _), _tok_colon), body) ]), _), @@ -108,16 +142,16 @@ class ['self] visitor = let body = self#visit_expr env body in let params = params_of_args args in make_funcdef ~tdef ~ident ~params ~guard:None ~tdo:None - ~body:([ body ]) ~tend:None ~def_str + ~body:([ body ]) ~tend:None ~rescue:[] ~def_str (* def foo(x) when guard do ... end *) | ( I (Id (( "def" | "defp" ) as def_str, tdef)), (_, ([ When (Call (I ident, args, None), _twhen, E guard) ], []), _), - Some (tdo, (Body body, []), tend) ) -> + Some (tdo, (Body body, extras), tend) ) -> let guard = self#visit_expr env guard in let body = self#visit_body env body in let params = params_of_args args in make_funcdef ~tdef ~ident ~params ~guard:(Some guard) ~tdo:(Some tdo) - ~body ~tend:(Some tend) ~def_str + ~body ~tend:(Some tend) ~rescue:extras ~def_str (* def foo(x) when guard, do: body *) | ( I (Id (( "def" | "defp" ) as def_str, tdef)), (_, ([ When (Call (I ident, args, None), _twhen, E guard) ], @@ -127,7 +161,7 @@ class ['self] visitor = let body = self#visit_expr env body in let params = params_of_args args in make_funcdef ~tdef ~ident ~params ~guard:(Some guard) ~tdo:None - ~body:([ body ]) ~tend:None ~def_str + ~body:([ body ]) ~tend:None ~rescue:[] ~def_str (* https://hexdocs.pm/elixir/Kernel.html#defmodule/2 *) | ( I (Id ("defmodule", tdefmodule)), (_, ([ Alias mname ], []), _), @@ -141,6 +175,40 @@ class ['self] visitor = } in S (D (ModuleDef def)) + (* https://hexdocs.pm/elixir/Kernel.SpecialForms.html#throw/1 *) + | ( I (Id ("throw", tthrow)), (_, ([ arg ], []), _), None ) -> + let arg = self#visit_expr env arg in + S (Throw (tthrow, arg)) + (* https://hexdocs.pm/elixir/Kernel.SpecialForms.html#try/1 *) + | ( I (Id ("try", ttry)), (_, ([], []), _), Some do_block ) -> + let do_block = self#visit_do_block env do_block in + S (Try (ttry, do_block)) + (* https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1 + * for pattern <- collection, filter, ... do body end *) + | ( I (Id ("for", tfor)), + (_, (args, _kwds), _), + Some (tdo, (Body body, []), tend) ) -> + let clauses = self#for_clauses env args in + let body = self#visit_body env body in + S (For (tfor, clauses, (tdo, body, tend))) + (* for pattern <- collection, do: body (compact keyword form) *) + | ( I (Id ("for", tfor)), + (_, (args, kwds), _), + None ) -> ( + match List.find_opt (fun (kwd : pair) -> + match kwd with + | Kw_expr ((X1 (do_kw, _), _), _) + when String.starts_with ~prefix:"do:" do_kw -> true + | _ -> false + ) kwds with + | Some (Kw_expr (_, body_expr)) -> + let clauses = self#for_clauses env args in + let body_expr = self#visit_expr env body_expr in + let fake = Tok.unsafe_fake_tok "" in + S (For (tfor, clauses, (fake, [ body_expr ], fake))) + | _ -> + let x = self#visit_call env x in + Call x) | _else_ -> let x = self#visit_call env x in Call x diff --git a/languages/elixir/generic/Elixir_to_generic.ml b/languages/elixir/generic/Elixir_to_generic.ml index c2ee1807c3..9be473fe15 100644 --- a/languages/elixir/generic/Elixir_to_generic.ml +++ b/languages/elixir/generic/Elixir_to_generic.ml @@ -369,6 +369,25 @@ and map_items env (v1, v2) : G.expr list = let v2 = map_keywords env v2 in v1 @ List_.map keyval_of_pair v2 +(* Like map_items but wraps each pair in OtherExpr to distinguish + * arrow syntax (%{"k" => v}) from keyword syntax (%{k: v}). *) +and map_map_items env (v1, v2) : G.expr list = + let v1 = (map_list map_expr) env v1 in + let v2 = map_keywords env v2 in + let wrap_arrow (e : G.expr) : G.expr = + match e.G.e with + | G.Ellipsis _ -> e + | _ -> G.OtherExpr (("MapPairArrow", G.fake "=>"), [ G.E e ]) |> G.e + in + let wrap_keyword p : G.expr = + match p with + | Right e -> e (* ellipsis or metavar, pass through unwrapped *) + | Left _ -> + let e = keyval_of_pair p in + G.OtherExpr (("MapPairKeyword", G.fake ":"), [ G.E e ]) |> G.e + in + List_.map wrap_arrow v1 @ List_.map wrap_keyword v2 + and map_keywords env v = (map_list map_pair) env v and map_pair_kw_expr env (v1, v2) = @@ -406,6 +425,36 @@ and map_stmt env (v : stmt) : G.stmt = in let st1 = G.Block (tdo, then_, tthenend) |> G.s in G.If (tif, G.Cond e, st1, elseopt) |> G.s + | Throw (tthrow, e) -> + let e = map_expr env e in + G.Throw (tthrow, e, G.sc) |> G.s + | For (tfor, clauses, (tdo, body, tend)) -> + let comp_clauses = List_.map (fun (clause : for_clause) -> + match clause with + | ForGenerator (pat, tarrow, collection) -> + let pat = map_expr env pat |> H.expr_to_pattern in + let collection = map_expr env collection in + G.CompFor (tfor, pat, tarrow, collection) + | ForFilter e -> + let e = map_expr env e in + G.CompIf (G.fake "if", e) + ) clauses in + let body_stmts = map_stmts env body in + let body_expr = + match body_stmts with + | [ { G.s = G.ExprStmt (e, _sc); _ } ] -> e + | _ -> G.stmt_to_expr (G.Block (tdo, body_stmts, tend) |> G.s) + in + let comp = G.Comprehension (G.List, (tdo, (body_expr, comp_clauses), tend)) in + G.ExprStmt (comp |> G.e, G.sc) |> G.s + | Try (ttry, (tdo, (boc, extras), tend)) -> + let body_stmts = + match boc with + | Body stmts -> map_stmts env stmts + | Clauses _ -> (* unreachable: try body is always Body stmts *) [] + in + let body_stmt = G.Block (tdo, body_stmts, tend) |> G.s in + wrap_with_rescue env ttry body_stmt extras | D def -> let d = map_definition env def in G.DefStmt d |> G.s @@ -445,12 +494,84 @@ and map_param_to_gparam env (p : parameter) : G.parameter = G.Param (G.param_of_id ?pdefault id)) | OtherParamExpr e -> let e = map_expr env e in - G.OtherParam (("OtherParamExpr", G.fake ""), [ G.E e ]) + G.ParamPattern (H.expr_to_pattern e) | OtherParamPair (kwd, e) -> let kwd = map_keyword env kwd in let e = map_expr env e in let e = keyval_of_pair (Left (kwd, e)) in - G.OtherParam (("OtherParamPair", G.fake ""), [ G.E e ]) + G.ParamPattern (H.expr_to_pattern e) + +(* Convert one rescue/catch stab clause to a G.catch arm. + * Each stab has a list of exception-type expressions and a handler body. *) +and map_rescue_stab_to_catch env tok (stab : stab_clause) : G.catch = + let ((args, _kwargs), guard_opt), _tarrow, body_stmts = stab in + let catch_pat = + match args with + | [] -> G.PatEllipsis tok + | [arg] -> + let e = map_expr env arg in + H.expr_to_pattern e + | args -> + let pats = List_.map (fun a -> H.expr_to_pattern (map_expr env a)) args in + let pat = + List.fold_right (fun p acc -> G.DisjPat (p, acc)) + (List.tl pats) (List.hd pats) + in + pat + in + let catch_pat = + match guard_opt with + | Some (_tok, guard) -> G.PatWhen (catch_pat, map_expr env guard) + | None -> catch_pat + in + let catch_exn = G.CatchPattern catch_pat in + let body = map_stmts env body_stmts in + (tok, catch_exn, G.Block (Tok.unsafe_fake_bracket body) |> G.s) + +(* Wrap a body statement in a G.Try when there are rescue/catch/after clauses. + * `tok` is used as the try token. *) +and wrap_with_rescue env tok body_stmt rescue = + match rescue with + | [] -> body_stmt + | extras -> + let catches, try_else, finally = + List.fold_left + (fun (catches, try_else, finally) ((kind, t), boc) -> + match (kind : exn_clause_kind) with + | Rescue | Catch -> + let s = + match boc with + | Clauses stabs -> + let cs = List_.map (map_rescue_stab_to_catch env t) stabs in + catches @ cs + | Body _ -> catches + in + (s, + try_else, + finally) + | Else -> + let s = + match boc with + | Clauses stabs -> + stabs |> List.concat_map (fun (_, _, b) -> map_stmts env b) + | Body _ -> [] + in + (catches, + Some (t, G.Block (Tok.unsafe_fake_bracket s) |> G.s), + finally) + | After -> + let s = + match boc with + | Clauses stabs -> + stabs |> List.concat_map (fun (_, _, b) -> map_stmts env b) + | Body stmts -> map_stmts env stmts + in + (catches, + try_else, + Some (t, G.Block (Tok.unsafe_fake_bracket s) |> G.s))) + ([], None, None) extras + in + G.Try (tok, body_stmt, catches, try_else, finally) |> G.s and map_func_clause_to_stab env (clause : function_definition) : stab_clause_generic = @@ -466,7 +587,7 @@ and map_func_clause_to_stab env (clause : function_definition) : and map_definition env (v : definition) : G.definition = match v with - | FuncDef [ { f_def; f_name; f_params; f_guard = None; f_body; f_is_private } ] -> + | FuncDef [ { f_def; f_name; f_params; f_guard = None; f_body; f_rescue; f_is_private } ] -> (* Single clause, no guard: use direct param names (original behavior). * This preserves the simple fparams=[x,y,...] representation so that * taint analysis can match call arguments to parameters by position @@ -483,12 +604,15 @@ and map_definition env (v : definition) : G.definition = let fparams = map_bracket (map_list map_param_to_gparam) env f_params in let l, body, r = f_body in let body_stmts = map_stmts env body in + let body_stmt = + wrap_with_rescue env l (G.Block (l, body_stmts, r) |> G.s) f_rescue + in let fdef = { G.fkind = (G.Function, f_def); fparams; frettype = None; - fbody = G.FBStmt (G.Block (l, body_stmts, r) |> G.s); + fbody = G.FBStmt body_stmt; } in (ent, G.FuncDef fdef) @@ -528,9 +652,10 @@ and map_definition env (v : definition) : G.definition = let stab = map_func_clause_to_stab env c in case_and_body_of_stab_clause stab) in - let body_stmt = - G.Switch (tk, Some (G.Cond switch_cond), cases) |> G.s - in + let switch_stmt = G.Switch (tk, Some (G.Cond switch_cond), cases) |> G.s in + (* Aggregate rescue/catch/after clauses from all function clauses *) + let all_rescue = List.concat_map (fun c -> c.f_rescue) clauses in + let body_stmt = wrap_with_rescue env tk switch_stmt all_rescue in let fdef = { G.fkind = (G.Function, tk); @@ -596,10 +721,30 @@ and map_binary_op env v1 v2 v3 = let e2 = map_expr env v3 in match op with | Left (op, tk) -> G.opcall (op, tk) [ e1; e2 ] + | Right (("=>", tk) as _id) -> + G.keyval e1 tk e2 | Right id -> let n = G.N (H.name_of_id id) |> G.e in G.Call (n, fb ([ e1; e2 ] |> List_.map G.arg)) |> G.e +(* Desugar pipe: x |> f(a, b) becomes f(x, a, b), tagged with + * OtherExpr("PipelineCall", ...) so search patterns can distinguish + * piped calls from direct calls. *) +and map_pipeline env (v1 : expr) (tk : tok) (v3 : expr) : G.expr = + let desugared = + match v3 with + | Call (fn, (l, (args, kwds), r), blk) -> + (* Insert v1 as first argument at the Elixir AST level; + * map_call will call map_expr on v1, handling nested pipes. *) + map_call env (fn, (l, (v1 :: args, kwds), r), blk) + | _ -> + (* Bare function reference: x |> f becomes f(x) *) + let e1 = map_expr env v1 in + let e2 = map_expr env v3 in + G.Call (e2, fb [ G.Arg e1 ]) |> G.e + in + G.OtherExpr (("PipelineCall", tk), [ G.E desugared ]) |> G.e + and map_match env v1 v2 = (* Single LHS names are VarDefs. * Otherwise, including ellipsis, we consider them LetPatterns. @@ -661,20 +806,17 @@ and map_expr env v : G.expr = |> G.e | Map (v1, v2, v3) -> ( let v2 = (map_option map_astruct) env v2 in - let l, xs, r = (map_bracket map_items) env v3 in + let l, xs, r = (map_bracket map_map_items) env v3 in + let dict_l = Tok.combine_toks v1 [ l ] in + let dict_body = G.Container (G.Dict, (dict_l, xs, r)) |> G.e in match v2 with - | None -> - let l = Tok.combine_toks v1 [ l ] in - G.Container (G.Dict, (l, xs, r)) |> G.e - | Some astruct -> - (* TODO? - | Some (Left id) -> - let n = H2.name_of_id id in - let ty = G.TyN n |> G.t in - G.New (tpercent, ty, G.empty_id_info (), (l, List_.map G.arg xs, r)) - |> G.e - *) - G.Call (astruct, (l, List_.map G.arg xs, r)) |> G.e) + | None -> dict_body + | Some name_expr -> ( + match H.name_of_dot_access name_expr with + | Some n -> G.Constructor (n, fb [ dict_body ]) |> G.e + | None -> + (* Fallback for dynamic struct names that can't be statically resolved *) + G.Call (name_expr, fb [ G.arg dict_body ]) |> G.e)) | Alias v -> (* TODO: split alias in components, and then use name_of_ids *) let v = map_alias env v in @@ -721,6 +863,7 @@ and map_expr env v : G.expr = | BinaryOp (v1, v2, v3) -> ( match v2 with | OMatch, _tk -> map_match env v1 v3 + | OPipeline, tk -> map_pipeline env v1 tk v3 | _else_ -> map_binary_op env v1 v2 v3) | OpArity (v1, tslash, pi) -> let id = map_wrap_operator_ident env v1 in @@ -735,7 +878,7 @@ and map_expr env v : G.expr = let e1 = map_expr env v1 in let v3 = map_expr_or_kwds env v3 in let e3 = expr_of_expr_or_kwds v3 in - G.OtherExpr (("Join", tbar), [ G.E e1; G.E e3 ]) |> G.e + G.Constructor (H.name_of_id ("|", tbar), fb [ e1; e3 ]) |> G.e | Lambda (tfn, v2, _tend) -> let xs = map_clauses env v2 in let fdef = stab_clauses_to_function_definition tfn xs in diff --git a/languages/elixir/tree-sitter/semgrep-elixir b/languages/elixir/tree-sitter/semgrep-elixir index 227a88dc16..681f900527 160000 --- a/languages/elixir/tree-sitter/semgrep-elixir +++ b/languages/elixir/tree-sitter/semgrep-elixir @@ -1 +1 @@ -Subproject commit 227a88dc166cab25eb9554f7186c8d53d39c96d8 +Subproject commit 681f90052720f4bc1c2fd2c2547771f6029917f0 diff --git a/languages/ruby/ast/ast_ruby.ml b/languages/ruby/ast/ast_ruby.ml index 3b3187192f..8463462836 100644 --- a/languages/ruby/ast/ast_ruby.ml +++ b/languages/ruby/ast/ast_ruby.ml @@ -213,7 +213,7 @@ type expr = | Ternary of expr * tok (* ? *) * expr * tok (* : *) * expr (* the brackets can be fake when the call is a "Command" *) | Call of expr * arguments bracket * expr option - (* TODO: ArrayAccess of expr * expr list bracket *) + | ArrayAccess of expr * arguments bracket (* old: was Binop(e1, Op_DOT, e2) before *) | DotAccess of expr * tok (* . or &. *) * method_name | DotAccessEllipsis of expr * tok (* '...' *) diff --git a/languages/ruby/generic/ruby_to_generic.ml b/languages/ruby/generic/ruby_to_generic.ml index f2c9a4182f..1c8264d698 100644 --- a/languages/ruby/generic/ruby_to_generic.ml +++ b/languages/ruby/generic/ruby_to_generic.ml @@ -126,6 +126,12 @@ let rec expr e = * and `f($X)` will match `f(x)`. *) let barg = b |> expr |> G.arg in G.Call (G.e e_call, (lb, [ barg ], rb))) + | ArrayAccess (e, (lb, args, rb)) -> + let e = expr e in + let args = list argument args in + let exprs = List.filter_map (function G.Arg e -> Some e | _ -> None) args in + let index = G.Container (G.Array, (lb, exprs, rb)) |> G.e in + G.ArrayAccess (e, (lb, index, rb)) | DotAccess (e, t, m) -> ( let e = expr e in match m with @@ -569,21 +575,10 @@ and expr_as_stmt = function | D x -> definition x | e -> ( let e = expr e in - match e.G.e with - (* targets only: a single name on its own line is probably an hidden fun call, - * unless it's a metavariable - *) - | G.N (G.Id ((s, _), _)) -> - if AST_generic.is_metavar_name s || (Domain.DLS.get Flag_parsing.sgrep_mode) then - G.exprstmt e - else - let call = G.Call (e, fb []) |> G.e in - G.exprstmt call - | _ -> ( - match expr_special_cases e with - | G.S s -> s - | G.E e -> G.exprstmt e - | _ -> raise Impossible)) + match expr_special_cases e with + | G.S s -> s + | G.E e -> G.exprstmt e + | _ -> raise Impossible) and stmt st = match st with diff --git a/languages/ruby/tree-sitter/Parse_ruby_tree_sitter.ml b/languages/ruby/tree-sitter/Parse_ruby_tree_sitter.ml index 0e2301b2a5..7a40bb20ed 100644 --- a/languages/ruby/tree-sitter/Parse_ruby_tree_sitter.ml +++ b/languages/ruby/tree-sitter/Parse_ruby_tree_sitter.ml @@ -2254,13 +2254,11 @@ and lhs (env : env) (x : CST.lhs) : AST.expr = | None -> [] in let v4 = token2 env v4 in - let e = - (* THINK: Why do we need a DotAccess here rather than just `v1' ? - * And why a Call rather than an ArrayAccess ? - *) - DotAccess (v1, v2, MethodOperator (Op_AREF, v2)) - in - Call (e, (v2, v3, v4), None) + (* Ruby `obj[key]` is element/hash access. Use ArrayAccess so + the receiver is a plain sub-expression that + Disambiguate_ruby_calls can wrap in Call() when it is an + unresolved bare identifier (zero-arg method call). *) + ArrayAccess (v1, (v2, v3, v4)) | `Call_ x -> ( let e = call_ env x in match e with diff --git a/libs/ast_generic/AST_generic_helpers.ml b/libs/ast_generic/AST_generic_helpers.ml index aaf73dce92..ea1ac9ca0f 100644 --- a/libs/ast_generic/AST_generic_helpers.ml +++ b/libs/ast_generic/AST_generic_helpers.ml @@ -201,9 +201,15 @@ let rec expr_to_pattern e = | L l -> PatLiteral l | Container (List, (t1, xs, t2)) -> PatList (t1, xs |> List_.map expr_to_pattern, t2) + | Container (Dict, (t1, xs, t2)) -> + PatList (t1, xs |> List_.map expr_to_pattern, t2) + | Constructor (n, (_, args, _)) -> + PatConstructor (n, args |> List_.map expr_to_pattern) | Ellipsis t -> PatEllipsis t + | OtherExpr (tag, [ E e ]) -> OtherPat (tag, [ P (expr_to_pattern e) ]) | Cast (ty, _tok, expr) -> PatTyped (expr_to_pattern expr, ty) - (* TODO: PatKeyVal and more *) + | LetPattern (p, {e = N (Id (i, info)); _} ) -> PatAs (p, (i, info)) + (* TODO: PatKeyVal and more *) | _ -> OtherPat (("ExprToPattern", fake ""), [ E e ]) exception NotAnExpr @@ -217,7 +223,10 @@ let rec pattern_to_expr p = | PatLiteral l -> L l | PatList (t1, xs, t2) -> Container (List, (t1, xs |> List_.map pattern_to_expr, t2)) + | PatConstructor (n, ps) -> + Constructor (n, Tok.unsafe_fake_bracket (ps |> List_.map pattern_to_expr)) | OtherPat (("ExprToPattern", _), [ E e ]) -> e.e + | OtherPat (tag, [ P p ]) -> OtherExpr (tag, [ E (pattern_to_expr p) ]) | _ -> raise NotAnExpr) |> G.e diff --git a/setup.py b/setup.py index 623e963cbf..689e0e114a 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="semgrep_pre_commit_package", - version="1.16.5", + version="1.19.0", # install_requires=["opengrep==1.2.0"], # Commented out since we're not on pypi. packages=[], ) diff --git a/src/analyzing/AST_to_IL.ml b/src/analyzing/AST_to_IL.ml index 2d5f31a80d..31762f98e7 100644 --- a/src/analyzing/AST_to_IL.ml +++ b/src/analyzing/AST_to_IL.ml @@ -237,12 +237,23 @@ let is_hcl lang : bool = | Lang.Terraform -> true | _ -> false +(* Extract the class name from a G.New type to build the constructor + reference in the IL New instruction. Only called from class_construction, + which only runs for G.New nodes (always constructor calls). + + The TyExpr case exists because the JS parser produces TyExpr where + it should produce TyN. + + Previously this had a guard `when Option.is_some !(cons_id_info.id_resolved)` + which meant it only worked when the pro engine's naming pass had run. + In OSS mode id_resolved is never set, so the IL New always got + constructor=None, falling through to all_args_taints (accidental leak). + The guard was removed because G.New is always a constructor call and + the fallback behavior is unchanged when no signature is found. *) let mk_class_constructor_name (ty : G.type_) cons_id_info : G.name option = match ty with | { t = TyN (G.Id (id, _)); _ } - | { t = TyExpr { e = G.N (G.Id (id, _)); _ }; _ } - (* FIXME: JS parser produces this ^ although it should be parsed as a 'TyN'. *) - when Option.is_some !(cons_id_info.G.id_resolved) -> + | { t = TyExpr { e = G.N (G.Id (id, _)); _ }; _ } -> Some (G.Id (id, cons_id_info)) | __else__ -> None @@ -425,6 +436,16 @@ and pattern env pat : stmts * lval * stmts = (* XXX: Assume same bound variables on lhs and rhs, as is imposed on most * languages. Hence we only recurse on one side. Seems good enough for now. *) pattern env pat1 + | G.OtherPat ((("MapPairArrow" | "MapPairKeyword"), _), [ G.P inner ]) + when env.lang =*= Lang.Elixir -> + pattern env inner + | G.OtherPat (("ExprToPattern", tok), [ G.E e ]) -> + (* expr_to_pattern fallback: the expression couldn't be statically + * converted to a known pattern. Evaluate the expression so that + * side-effects and taint flow are captured, then bind a fresh tmp. *) + let pre_ss, _e' = expr env e in + let tmp = fresh_lval tok in + (pre_ss, tmp, []) | G.PatEllipsis _ -> sgrep_construct (G.P pat) | _ -> todo (G.P pat) @@ -959,9 +980,8 @@ and expr_aux env ?(void = false) g_expr : stmts * exp = let xs = List.map snd results in let kind = composite_kind ~g_expr kind in (ss, mk_e (Composite (kind, (l, xs, r))) eorig) - | G.Comprehension (_op, (_l, (er, [CompFor (tok_for, pat, tok_in, e)]), _r)) -> - comprehension_for env er tok_for pat tok_in e - | G.Comprehension _ -> todo (G.E g_expr) + | G.Comprehension (_op, (_l, (er, clauses), _r)) -> + comprehension env er clauses | G.Lambda fdef -> let lval = fresh_lval (snd fdef.fkind) in let final_fdef = @@ -1069,6 +1089,12 @@ and expr_aux env ?(void = false) g_expr : stmts * exp = | G.OtherExpr (("ShortLambda", _), _) when env.lang =*= Lang.Elixir -> let lambda_expr = AST_modifications.convert_elixir_short_lambda g_expr in expr env lambda_expr + (* Elixir pipe: OtherExpr("PipelineCall", [E call]) is a desugared + * x |> f(a) => f(x, a). The tag preserves search distinction; for + * IL/taint we evaluate the inner call transparently. *) + | G.OtherExpr (("PipelineCall", _tk), [ G.E inner ]) + when env.lang =*= Lang.Elixir -> + expr env inner (* The idea here is that this is like a block, and we only * really care about the last expression. *) (* TODO: What if a statement creeps in? E.g. an If, `fn`..? @@ -1451,6 +1477,14 @@ and dict env (_, orig_entries, _) orig : stmts * exp = let ss_k, ke = expr env korig in let ss_v, ve = expr env vorig in (ss_k @ ss_v, Entry (ke, ve)) + | G.OtherExpr ((("MapPairArrow" | "MapPairKeyword"), _), [ G.E inner ]) + when env.lang =*= Lang.Elixir -> + (match inner.G.e with + | G.Container (G.Tuple, (_, [ korig; vorig ], _)) -> + let ss_k, ke = expr env korig in + let ss_v, ve = expr env vorig in + (ss_k @ ss_v, Entry (ke, ve)) + | _ -> todo (G.E orig)) | __else__ -> todo (G.E orig)) orig_entries in @@ -1571,9 +1605,10 @@ and xml_expr env ~void eorig xml : stmts * exp = (CTuple, (tok, List.rev_append filtered_attrs filtered_body, tok))) (Related (G.Xmls xml.G.xml_body)) ) -(* Adjusted from for_each *) -and comprehension_for env result_expr tok_for pat tok_in collection_expr : stmts * exp = - let tmp = fresh_lval ~str:"_comprehension_tmp" tok_in in +(* Build a single foreach loop around [inner_body] IL stmts. + * Adjusted from for_each; used by [comprehension] below. *) +and comprehension_loop env tok_for (pat : G.pattern) (tok_in : tok) + (collection_expr : G.expr) (inner_body : stmts) : stmts = let cont_label_s, break_label_s, env = break_continue_labels env tok_for in let ss, e' = expr env collection_expr in let next_lval = fresh_lval tok_in in @@ -1598,22 +1633,44 @@ and comprehension_for env result_expr tok_for pat tok_in collection_expr : stmts (mk_e (Fetch next_lval) (related_tok tok_in)) ~eorig:(related_tok tok_in) pat in + let cond = mk_e (Fetch hasnext_lval) (related_tok tok_in) in + ss @ + [ hasnext_call; + mk_s + (Loop + ( tok_in, + cond, + next_call :: assign_st @ inner_body @ cont_label_s + @ [ hasnext_call ] )); + ] + @ break_label_s + +(* Recursively build nested loops/guards from comprehension clauses, + * wrapping [inner_body] at the innermost level. + * Mirrors the MultiForEach nesting in [stmt]. *) +and comprehension_clauses env (clauses : G.for_or_if_comp list) + (inner_body : stmts) : stmts = + match clauses with + | [] -> inner_body + | G.CompFor (tok_for, pat, tok_in, collection_expr) :: rest -> + let body = comprehension_clauses env rest inner_body in + comprehension_loop env tok_for pat tok_in collection_expr body + | G.CompIf (tok_if, guard_expr) :: rest -> + let body = comprehension_clauses env rest inner_body in + let ss, e' = expr env guard_expr in + ss @ [ mk_s (If (tok_if, e', body, [])) ] + +(* Compile a comprehension: create an accumulator, build nested + * loops from the clause list, append each result to the accumulator. *) +and comprehension env (result_expr : G.expr) + (clauses : G.for_or_if_comp list) : stmts * exp = + let tok = G.fake "comprehension" in + let tmp = fresh_lval ~str:"_comprehension_tmp" tok in let ss_res, e_eres = expr env ~void:false result_expr in let e_plus = mk_e (Operator ((G.Plus, Tok.unsafe_fake_tok "+="), [Unnamed e_eres])) NoOrig in - let st = mk_s (Instr (mk_i (Assign (tmp, e_plus)) NoOrig)) in - let cond = mk_e (Fetch hasnext_lval) (related_tok tok_in) in - let loop_stmts = - ss @ - [ hasnext_call; - mk_s - (Loop - ( tok_in, - cond, - next_call :: assign_st @ ss_res @ [st] @ cont_label_s - @ [ hasnext_call ] )); - ] - @ break_label_s - in + let append_st = mk_s (Instr (mk_i (Assign (tmp, e_plus)) NoOrig)) in + let inner_body = ss_res @ [ append_st ] in + let loop_stmts = comprehension_clauses env clauses inner_body in (loop_stmts, mk_e (Fetch tmp) NoOrig) and stmt_expr env ?g_expr st : stmts * exp = diff --git a/src/call_graph/Call_graph.ml b/src/call_graph/Call_graph.ml index 3d5b244a7f..bb14a91423 100644 --- a/src/call_graph/Call_graph.ml +++ b/src/call_graph/Call_graph.ml @@ -51,7 +51,9 @@ module Display = struct let default_vertex_attributes _ = [] let vertex_attributes _ = [] let default_edge_attributes _ = [] - let edge_attributes _ = [] + let edge_attributes (_, label, _) = + let pos = label.call_site in + [ `Label (Printf.sprintf "l:%d c:%d" pos.Pos.line pos.Pos.column) ] let get_subgraph _ = None end diff --git a/src/call_graph/Function_id.ml b/src/call_graph/Function_id.ml index 5b6c7677f3..991fcc6364 100644 --- a/src/call_graph/Function_id.ml +++ b/src/call_graph/Function_id.ml @@ -53,6 +53,8 @@ let compare (n1 : t) (n2 : t) : int = let equal (n1 : t) (n2 : t) : bool = key n1 = key n2 +let tok ((_, tok) : t) : Tok.t = tok + let show ((id, _) : t) : string = id diff --git a/src/call_graph/Function_id.mli b/src/call_graph/Function_id.mli index 30eb9f1cda..432669024a 100644 --- a/src/call_graph/Function_id.mli +++ b/src/call_graph/Function_id.mli @@ -12,4 +12,6 @@ val show_debug : t -> string val of_il_name : IL.name -> t +val tok : t -> Tok.t + val to_file_line_col : t -> string * int * int diff --git a/src/core/Version.ml b/src/core/Version.ml index 57ef77ca3e..059760d159 100644 --- a/src/core/Version.ml +++ b/src/core/Version.ml @@ -3,5 +3,5 @@ Automatically modified by scripts/release/bump. *) -let version = "1.16.5" +let version = "1.19.0" let version_semgrep = "1.100.0" diff --git a/src/matching/Generic_vs_generic.ml b/src/matching/Generic_vs_generic.ml index b57800d89b..37e006cfb8 100644 --- a/src/matching/Generic_vs_generic.ml +++ b/src/matching/Generic_vs_generic.ml @@ -952,6 +952,15 @@ and m_expr ?(is_root = false) ?(arguments_have_changed = true) a b = | G.N (G.Id _ as na), B.N (B.Id _ as nb) -> m_name na nb >||> m_with_symbolic_propagation ~is_root (fun b1 -> m_expr a b1) b + (* Ruby equivalence: bare identifier `foo` matches zero-arg call `foo()`. + After disambiguation, target bare identifiers become Call(N(Id(x)), []) + but patterns keep the bare N(Id(x)) form. Exclude metavars so they + fall through to the general metavar case below. *) + | G.N (G.Id ((str, _), _) as na), B.Call ({ e = B.N (B.Id _ as nb); _ }, (_, [], _)) + when not (Mvar.is_metavar_name str) -> + with_lang (fun lang -> + if lang =*= Lang.Ruby then m_name na nb + else fail ()) | G.N (G.Id ((str, tok), _id_info)), _b when Mvar.is_metavar_name str -> envf (str, tok) (MV.E b) (* metavar: typed! *) diff --git a/src/parsing/Unit_parsing.ml b/src/parsing/Unit_parsing.ml index a553a69e94..7ace242c93 100644 --- a/src/parsing/Unit_parsing.ml +++ b/src/parsing/Unit_parsing.ml @@ -119,13 +119,16 @@ let pack_parsing_tests_for_lang ?(error_tolerance = Strict) lang = let pattern = spf "%s/**/*" !!dir in let files = Common2.glob pattern in if files =*= [] then - failwith (spf "Empty set of parsing tests for %s at %s" slang pattern); - List.iter check_ext files; - let tests = parsing_tests_for_lang error_tolerance files lang in - (match subcategory with - | None -> tests - | Some cat -> Testo.categorize cat tests) - |> Testo.categorize slang + (* No test files found; return an empty suite rather than crashing *) + [] + else begin + List.iter check_ext files; + let tests = parsing_tests_for_lang error_tolerance files lang in + (match subcategory with + | None -> tests + | Some cat -> Testo.categorize cat tests) + |> Testo.categorize slang + end (* Note that here we also use tree-sitter to parse; certain files were not * parsing with pfff but parses here @@ -221,7 +224,9 @@ let make_tests langs_with_tolerance = let langs_with_error_tolerance = [ (* languages with only a tree-sitter parser *) + (Lang.Apex, Strict); (Lang.Bash, Strict); + (Lang.Elixir, Strict); (Lang.Csharp, Strict); (Lang.Dockerfile, Strict); (Lang.Lua, Strict); @@ -240,11 +245,17 @@ let langs_with_error_tolerance = (Lang.Julia, Strict); (Lang.Jsonnet, Strict); (Lang.Dart, Strict); + (Lang.Json, Strict); + (* TODO: Move_on_sui has non-.move files in its test dir (TODO/) *) + (* (Lang.Move_on_sui, Strict); *) + (Lang.Ql, Strict); (* here we have both a Pfff and tree-sitter parser *) (Lang.Java, Strict); (Lang.Go, Strict); (Lang.Ruby, Strict); (Lang.Js, Strict); + (Lang.Ts, Partial_parsing); + (Lang.Python, Strict); (Lang.C, Strict); (Lang.Cpp, Strict); (Lang.Php, Strict); diff --git a/src/tainting/Dataflow_tainting.ml b/src/tainting/Dataflow_tainting.ml index 72f0666f6c..b7b9e08b66 100644 --- a/src/tainting/Dataflow_tainting.ml +++ b/src/tainting/Dataflow_tainting.ml @@ -429,8 +429,22 @@ let type_of_lval env lval = Typing.resolved_type_of_id_info env.taint_inst.lang fld.id_info | __else__ -> Type.NoType +let java_type_of_expr_is_unreliable_for_deep_chain (eorig : G.expr) = + match eorig.G.e with + | G.Call + ( { G.e = G.DotAccess ({ G.e = (G.Call _ | G.DotAccess _); _ }, _, _); _ }, + _ ) -> + true + | G.DotAccess ({ G.e = (G.Call _ | G.DotAccess _); _ }, _, _) -> + true + | _ -> false + let type_of_expr env e = match e.eorig with + | SameAs eorig + when env.taint_inst.lang =*= Lang.Java + && java_type_of_expr_is_unreliable_for_deep_chain eorig -> + Type.NoType | SameAs eorig -> Typing.type_of_expr env.taint_inst.lang eorig |> fst | __else__ -> Type.NoType @@ -638,13 +652,27 @@ let effects_of_call_func_arg fun_exp fun_shape args_taints = [] let get_signature_for_object graph caller_node db method_name ~call_tok arity = - (* Method call: obj.method() *) - (* First try to look up via call graph to get the correct node with definition token *) - match Call_graph.lookup_callee_from_graph - graph (Option.map Function_id.of_il_name caller_node) call_tok with + let caller = Option.map Function_id.of_il_name caller_node in + let method_tok = Function_id.tok method_name in + let lookup tok = Call_graph.lookup_callee_from_graph graph caller tok in + let lookup_signature_for_tok tok = + match lookup tok with + | Some callee_node -> + Shape_and_sig.(lookup_signature db callee_node arity) + | None -> None + in + let same_tok = Tok.equal call_tok method_tok in + match lookup_signature_for_tok method_tok with | Some callee_node -> - Shape_and_sig.(lookup_signature db callee_node arity) - | None -> Shape_and_sig.lookup_signature db method_name arity + Some callee_node + | None -> ( + match + if same_tok || Tok.is_fake call_tok then None + else lookup_signature_for_tok call_tok + with + | Some callee_node -> + Some callee_node + | None -> Shape_and_sig.lookup_signature db method_name arity) (* Helper to fallback to builtin signature database if regular lookup fails *) let try_builtin_fallback env func_name arity result = @@ -731,7 +759,9 @@ let lookup_signature_with_object_context env fun_exp arity = (* Try to look up via call graph using the ORIGINAL AST token position. This handles temp variables like _tmp:N which have eorig pointing to the actual callback reference in the original AST. *) - let call_tok = call_tok_of_fun_exp ~default_tok:(snd name.ident) fun_exp in + let call_tok = + call_tok_of_fun_exp ~default_tok:(snd name.ident) fun_exp + in let graph_lookup = Call_graph.lookup_callee_from_graph env.call_graph @@ -793,21 +823,24 @@ let lookup_signature_with_object_context env fun_exp arity = } when Option.is_some env.class_name -> ( (* Method call on self/this: self.method() or this.method() *) - (* First try to look up via call graph to get the correct fn_id *) + let method_tok = snd method_name.ident in let call_tok = - if Tok.is_fake (snd method_name.ident) then self_tok - else snd method_name.ident + if Tok.is_fake method_tok then self_tok else method_tok in - match - Call_graph.lookup_callee_from_graph - env.call_graph + let lookup tok = + Call_graph.lookup_callee_from_graph env.call_graph (Option.map Function_id.of_il_name env.func.name) - call_tok - with + tok + in + match lookup call_tok with | Some callee_node -> Shape_and_sig.(lookup_signature db callee_node arity) - | None -> - lookup_direct_or_by_name method_name) + | None -> ( + match lookup method_tok with + | Some callee_node -> + Shape_and_sig.(lookup_signature db callee_node arity) + | None -> + lookup_direct_or_by_name method_name)) | Fetch { base = Var obj; rev_offset = [ { o = Dot method_name; _ } ] } -> ( match get_signature_for_object @@ -860,6 +893,9 @@ let receiver_lval_for_constructor_call env lval_opt fun_exp = Some (call_tok_of_fun_exp ~default_tok:(snd name.ident) fun_exp) | Fetch { base = VarSpecial ((Self | This), self_tok); rev_offset = _ } -> Some (call_tok_of_fun_exp ~default_tok:self_tok fun_exp) + | Fetch { base = Var obj; rev_offset = { o = Dot method_name; _ } :: _ } + when looks_like_constructor_name (fst method_name.ident) -> + Some (snd obj.ident) | Fetch { base = Var obj; rev_offset = _ } -> Some (call_tok_of_fun_exp ~default_tok:(snd obj.ident) fun_exp) | _ -> None @@ -883,6 +919,9 @@ let receiver_lval_for_constructor_call env lval_opt fun_exp = let callee_name = fst name.ident in looks_like_constructor_name callee_name || bare_call_name_looks_like_constructor callee_name + | Fetch { base = Var obj; rev_offset = { o = Dot method_name; _ } :: _ } -> + Object_initialization.is_constructor env.taint_inst.lang + (fst method_name.ident) (Some (fst obj.ident)) | _ -> false in if resolved_constructor || syntactic_fallback then Some receiver_lval @@ -1384,10 +1423,17 @@ and check_tainted_lval_aux env (lval : IL.lval) : effects_of_tainted_sinks { env with lval_env } all_taints sinks in record_effects { env with lval_env } effects; + let sub_taints = + (* Preserve freshly-computed taint on the receiver sub-lvalue, not just + * taint already materialized in the environment. Chained method calls + * such as `obj.getItems(key).get(0)` rely on the intermediate receiver + * carrying taint into the next call within the same expression. *) + Taints.union sub_new_taints (Xtaint.to_taints sub_in_env) + in ( new_taints, lval_in_env, lval_shape, - `Sub (Xtaint.to_taints sub_in_env, sub_shape), + `Sub (sub_taints, sub_shape), lval_env ) and check_tainted_lval_base env base = @@ -1770,7 +1816,8 @@ let check_function_call ?receiver_lval env fun_exp args try let callee_name_opt = match callee.e with - | Fetch { base = Var name; rev_offset = [] } -> Some name + | Fetch { base = Var name; rev_offset = [] } + | Fetch { base = Var name; rev_offset = [{ o = Dot _; _ }] } -> Some name | _ -> None in match callee_name_opt with @@ -1904,6 +1951,49 @@ let call_with_intrafile lval_opt e env args instr = let e_obj, e_taints, e_shape, lval_env = check_function_call_callee { env with lval_env } e in + let apply_java_getter_result_fallback call_taints shape = + let getter_mode_opt = + match e.e with + | Fetch { rev_offset = { o = Dot name; _ } :: _; _ } -> + let method_name = fst name.ident in + if env.taint_inst.lang =*= Lang.Java + && String.length method_name > 3 + && String.starts_with ~prefix:"get" method_name + then + Some + (if List.length args > 0 then `Preserve_incoming else `Fallback_only) + else None + | _ -> None + in + match getter_mode_opt with + | None -> (call_taints, shape) + | Some getter_mode -> + let receiver_taints = + match e_obj with + | `Obj (obj_taints, _) -> obj_taints + | `Fun -> Taints.empty + in + let fallback_taints = receiver_taints |> Taints.union all_args_taints in + if Taints.is_empty fallback_taints then (call_taints, shape) + else + match getter_mode with + | `Preserve_incoming -> + (* Java accessor-style methods with parameters often behave like + * lookups/selectors. Even if intrafile signatures say "return a + * field", semgrep-rules expects us to conservatively preserve the + * tainted receiver/selector arguments through the result. *) + (Taints.union call_taints fallback_taints, shape) + | `Fallback_only -> + let no_precise_result = + Taints.is_empty call_taints + && + match shape with + | S.Bot -> true + | _ -> false + in + if not no_precise_result then (call_taints, shape) + else (Taints.union call_taints fallback_taints, shape) + in check_orig_if_sink { env with lval_env } instr.iorig all_args_taints Bot ~filter_sinks:(fun m -> not (m.spec.sink_exact && m.spec.sink_has_focus)); let call_taints, shape, lval_env = @@ -2093,6 +2183,75 @@ let call_with_intrafile lval_opt e env args instr = | None -> (all_args_taints, S.Bot, lval_env))) | None -> + (* Constructor call handling for ClassName() and ClassName.new(). + * + * When taint flows through a constructor (e.g., `obj = Foo(tainted)`), + * the constructor signature may contain ToLval(BThis.field, taint) + * effects that assign taint to fields of the new object. For Sig_inst + * to correctly map BThis onto the target variable `obj`, we need the + * callee expression to be `obj.Constructor` rather than just + * `Constructor`. We check the call graph to determine if this call + * resolves to a constructor, and if so, remap the callee accordingly. *) + let resolves_to_constructor = + (* Method calls on objects (e.g., _tmp.get_data()) should not be + remapped as constructors. Their eorig may share a token with a + constructor edge (e.g., in Passthrough(source()).get_data(), both + the constructor and the method eorig start at "Passthrough"). + Skip the constructor check for Dot accesses unless it's Ruby's + ClassName.new() pattern. *) + (match e.e with + | Fetch { rev_offset = [{ o = Dot name; _ }]; _ } + when fst name.IL.ident <> "new" + || not Lang.(env.taint_inst.lang =*= Ruby) -> false + | _ -> true) && + Option.is_some env.signature_db && + (* The constructor edge is stored at the class name token position + (first token of the call expression). Extract it from the callee. *) + let call_tok = match e.e with + | Fetch { base = Var name; _ } -> snd name.ident + | _ -> Tok.unsafe_fake_tok "" + in + not (Tok.is_fake call_tok) && + match Call_graph.lookup_callee_from_graph + env.call_graph + (Option.map Function_id.of_il_name env.func.name) + call_tok with + | Some callee_node -> + Object_initialization.is_constructor env.taint_inst.lang + (Function_id.show callee_node) None + | None -> false + in + (* Remap: ClassName() → obj.ClassName(), ClassName.new() → obj.ClassName() + * This makes the callee a method-call shape so that Sig_inst maps + * BThis to obj (the assignment target) when instantiating the + * constructor's ToLval effects. *) + let e = + if resolves_to_constructor then + match (lval_opt, e.e) with + | Some lval, Fetch { base = Var name; rev_offset = ([] | [{ o = Dot _; _ }]) } -> + IL.{ e = Fetch { base = lval.base; + rev_offset = [{ o = Dot name; oorig = NoOrig }] }; + eorig = e.eorig } + | _ -> e + else e + in + (* Python's __init__ has an explicit `self` parameter but constructor + * call sites (e.g., `Foo(x)`) don't pass it. Prepend the receiver + * variable so Sig_inst maps self → obj and user_name → x correctly. + * Ruby's initialize does NOT have explicit self, so this is + * Python-specific. *) + let args, args_taints = + if resolves_to_constructor + && Lang.(env.taint_inst.lang =*= Python) then + match lval_opt with + | Some lval -> + let self_exp = IL.{ e = Fetch lval; eorig = NoOrig } in + let self_arg = IL.Unnamed self_exp in + let self_taint = IL.Unnamed (Taints.empty, S.Bot) in + (self_arg :: args, self_taint :: args_taints) + | None -> (args, args_taints) + else (args, args_taints) + in (* No implicit lambda, try unified constructor execution *) let receiver_lval = receiver_lval_for_constructor_call env lval_opt e @@ -2104,7 +2263,32 @@ let call_with_intrafile lval_opt e env args instr = Object_initialization.execute_unified_constructor e args args_taints check_function_call_wrapper { env with lval_env } with - | Some (call_taints, shape, lval_env) -> (call_taints, shape, lval_env) + | Some (call_taints, shape, lval_env) -> + (* Constructor ToLval effects (e.g., this.data = tainted_arg) + * update lval_env with field-level taint on the target variable, + * but the return shape may still be Bot (constructors typically + * don't return a value). Read back the shape from lval_env so + * it propagates through intermediate assignments like + * `_tmp = Foo(x); obj = _tmp`. Without this, the shape is lost + * at the assignment boundary. *) + let shape = + if resolves_to_constructor then + match lval_opt with + | Some lval -> ( + match Lval_env.find_lval lval_env lval with + | Some (S.Cell (_, s)) when + (match s with + | S.Bot -> false + | _ -> true) -> s + | _ -> shape) + | None -> shape + else shape + in + let call_taints, shape = + if resolves_to_constructor then (call_taints, shape) + else apply_java_getter_result_fallback call_taints shape + in + (call_taints, shape, lval_env) | None -> ( (* Regular function call processing *) Log.debug (fun m -> @@ -2119,6 +2303,9 @@ let call_with_intrafile lval_opt e env args instr = (Display_IL.string_of_exp e) (T.show_taints call_taints) (S.show_shape shape)); + let call_taints, shape = + apply_java_getter_result_fallback call_taints shape + in (call_taints, shape, lval_env) | None -> ( Log.debug (fun m -> diff --git a/src/tainting/Graph_from_AST.ml b/src/tainting/Graph_from_AST.ml index 197237296c..9389f7dd36 100644 --- a/src/tainting/Graph_from_AST.ml +++ b/src/tainting/Graph_from_AST.ml @@ -489,6 +489,22 @@ let rec callsite_tok_of_callee_expr (expr : G.expr) : Tok.t option = | tok :: _ when not (Tok.is_fake tok) -> Some tok | _ -> None) +let callsite_tok_for_call ~(lang : Lang.t) (call_expr : G.expr) + (callee : G.expr) : Tok.t = + match callee.G.e with + | G.DotAccess (_, _, G.FN (G.Id (("new", _), _))) + when Lang.(lang =*= Ruby) -> ( + match AST_generic_helpers.ii_of_any (G.E call_expr) with + | tok :: _ -> tok + | [] -> Tok.unsafe_fake_tok "") + | _ -> ( + match callsite_tok_of_callee_expr callee with + | Some tok -> tok + | None -> ( + match AST_generic_helpers.ii_of_any (G.E call_expr) with + | tok :: _ -> tok + | [] -> Tok.unsafe_fake_tok "")) + (* Extract Go receiver type from method *) let extract_go_receiver_type (fdef : G.function_definition) : string option = let params = Tok.unbracket fdef.fparams in @@ -586,7 +602,74 @@ let dedup_fn_ids (ids : (fn_id * Tok.t) list) : (fn_id * Tok.t) list = if cmp <> 0 then cmp else Tok.compare t1 t2) (* Helper function to identify the callee fn_id from a call expression's callee *) -let identify_callee ?(object_mappings = []) ?(all_funcs = []) +let resolve_constructor_from_type ~(lang : Lang.t) ~all_funcs + ?(imported_entity_index = CanonicalMap.empty) + ?(current_file : Fpath.t option) (ty : G.type_) : fn_id option = + let is_constructor_fn_id fn_id = + match fn_id with + | [ Some cls; Some meth ] -> + let class_name = fst cls.IL.ident in + Object_initialization.is_constructor lang (fst meth.IL.ident) + (Some class_name) + | _ -> false + in + let is_local_function func = + match current_file with + | Some file -> matches_current_file file func + | None -> true + in + let lookup_local_constructor class_name = + List.find_opt + (fun f -> + is_local_function f + && + match f.fn_id with + | [ Some c; Some m ] -> + fst c.IL.ident = class_name + && Object_initialization.is_constructor lang (fst m.IL.ident) + (Some class_name) + | _ -> false) + all_funcs + |> Option.map (fun f -> f.fn_id) + in + let canonicals, class_name_opt = + match ty.G.t with + | G.TyN (G.Id ((name, _), id_info)) + | G.TyExpr { G.e = G.N (G.Id ((name, _), id_info)); _ } -> + let canonicals = + match !(id_info.G.id_resolved) with + | Some (G.ImportedEntity canonical, _sid) + | Some (G.ImportedModule canonical, _sid) -> [ canonical ] + | _ -> [ [ name ] ] + in + (canonicals, Some name) + | G.TyExpr + { G.e = G.N (G.IdQualified ({ name_info; _ } as qualified_info)); _ } -> + let canonical = + match !(name_info.G.id_resolved) with + | Some (G.ImportedEntity canonical, _sid) + | Some (G.ImportedModule canonical, _sid) -> canonical + | _ -> + AST_generic_helpers.dotted_ident_of_name + (G.IdQualified qualified_info) + |> List_.map fst + in + ([ canonical ], List_.last_opt canonical) + | _ -> ([], None) + in + match + canonicals + |> List.find_map (fun canonical -> + match lookup_imported_func ?current_file imported_entity_index canonical with + | Some func when is_constructor_fn_id func.fn_id -> Some func.fn_id + | Some _ + | None -> + None) + with + | Some _ as result -> result + | None -> Option.bind class_name_opt lookup_local_constructor + +let identify_callee ~(lang : Lang.t) ?(object_mappings = []) ?(all_funcs = []) ?(import_binding_timeline = StringMap.empty) ?(local_import_binding_timeline = StringMap.empty) ?(imported_entity_index = CanonicalMap.empty) ?(current_file : Fpath.t option) @@ -719,18 +802,36 @@ let identify_callee ?(object_mappings = []) ?(all_funcs = []) | G.N (G.Id ((id, _), id_info)) -> ( match resolve_local_function_call id with | Some _ as result -> result - | None -> ( - match !(id_info.G.id_resolved) with - | Some (G.ImportedEntity canonical, _sid) when is_import_binding_active id -> - lookup_imported_entity ?current_file imported_entity_index - canonical - | Some (G.ImportedModule _, _sid) -> None - | None when is_import_binding_active id -> - lookup_imported_entity ?current_file imported_entity_index - [ id ] - | Some _ + | None -> + let imported_result = + match !(id_info.G.id_resolved) with + | Some (G.ImportedEntity canonical, _sid) + when is_import_binding_active id -> + lookup_imported_entity ?current_file imported_entity_index + canonical + | Some (G.ImportedModule _, _sid) -> None + | None when is_import_binding_active id -> + lookup_imported_entity ?current_file imported_entity_index + [ id ] + | Some _ + | None -> + None + in + (match imported_result with + | Some _ as result -> result | None -> - None)) + let ty = + G. + { + t = + TyN + (G.Id + ((id, G.fake id), G.empty_id_info ())); + t_attrs = []; + } + in + resolve_constructor_from_type ~lang ~all_funcs + ~imported_entity_index ?current_file ty)) (* Qualified call: Module.foo() *) | G.N (G.IdQualified @@ -804,77 +905,98 @@ let identify_callee ?(object_mappings = []) ?(all_funcs = []) (canonical_module @ [ id ]) | None -> None) | G.DotAccess - ( { e = G.N (G.Id ((_obj_name, _), obj_info)); _ }, + ( { e = G.N (G.Id ((obj_name, _), obj_id_info)); _ }, _, G.FN (G.Id ((id, _), _id_info)) ) -> ( - match !(obj_info.G.id_resolved) with + match !(obj_id_info.G.id_resolved) with | Some (G.ImportedModule canonical_module, _sid) - when is_import_binding_active _obj_name -> + when is_import_binding_active obj_name -> lookup_imported_entity ?current_file imported_entity_index (canonical_module @ [ id ]) | Some (G.ImportedEntity canonical_entity, _sid) - when is_import_binding_active _obj_name -> + when is_import_binding_active obj_name -> lookup_imported_entity ?current_file imported_entity_index (canonical_entity @ [ id ]) | Some _ | None -> let method_name_str = id in - (* First try: treat obj as a module name and look up in - imported entities. This handles bare `import runner` in - Python where the naming pass does not set ImportedModule. *) let module_lookup = - if is_import_binding_active _obj_name then + if is_import_binding_active obj_name then lookup_imported_entity ?current_file imported_entity_index - [ _obj_name; method_name_str ] + [ obj_name; method_name_str ] else None in (match module_lookup with | Some _ as result -> result | None -> - (* Fall back: look up obj's class in object_mappings *) - let obj_class_opt = - object_mappings - |> List.find_opt (fun (var_name, _class_name) -> - match var_name with - | G.Id ((var_str, _), _) -> var_str = _obj_name - | _ -> false) - |> Option.map (fun (_var_name, class_name) -> class_name) - in - (match obj_class_opt with - | Some class_name -> - let class_name_str = match class_name with - | G.Id ((str, _), _) -> str - | _ -> "" + let obj_resolved = !(obj_id_info.G.id_resolved) in + let obj_class_opt = + object_mappings + |> List.find_opt (fun (var_name, _class_name) -> + match var_name with + | G.Id ((var_str, _), var_id_info) -> + var_str = obj_name + && + (match + (obj_resolved, !(var_id_info.G.id_resolved)) + with + | Some (_, sid1), Some (_, sid2) -> + G.SId.equal sid1 sid2 + | _ -> true) + | _ -> false) + |> Option.map (fun (_var_name, class_name) -> class_name) in - let imported_method = - lookup_imported_entity ?current_file imported_entity_index - [ class_name_str; method_name_str ] + let obj_class_opt = + match obj_class_opt with + | Some _ -> obj_class_opt + | None -> ( + match !(obj_id_info.G.id_type) with + | Some { G.t = G.TyN (G.Id _ as n); _ } + | Some + { + G.t = G.TyExpr { G.e = G.N (G.Id _ as n); _ }; + _; + } -> + Some n + | _ -> None) in - (* Find all methods matching class and name *) - let method_matches = List.filter (fun f -> - is_local_function f && - match f.fn_id with - | [Some c; Some m] when fst c.IL.ident = class_name_str && fst m.IL.ident = method_name_str -> true - | _ -> false - ) all_funcs in - (match imported_method with - | Some _ as result -> result - | None -> - match method_matches with - | [single_match] -> Some single_match.fn_id (* Exactly one match by name *) - | [] -> None - | _ -> - (* Multiple matches - filter by arity if available *) - (match call_arity with - | Some arity -> - let arity_matches = List.filter (fun f -> - Int.equal (get_func_arity f.fdef) arity - ) method_matches in - (match arity_matches with - | [single_match] -> Some single_match.fn_id - | _ -> None) (* Still 0 or multiple matches *) - | None -> None)) (* No arity info, can't disambiguate *) - | None -> None))) + (match obj_class_opt with + | Some class_name -> + let class_name_str = + match class_name with + | G.Id ((str, _), _) -> str + | _ -> "" + in + let imported_method = + lookup_imported_entity ?current_file + imported_entity_index + [ class_name_str; method_name_str ] + in + (match imported_method with + | Some _ as result -> result + | None -> + resolve_method_call_in_class class_name_str + method_name_str) + | None + when Object_initialization.is_constructor lang + method_name_str (Some obj_name) + || ((not + (Object_initialization.uses_new_keyword lang)) + && String.equal method_name_str "new") -> + let ty = + G. + { + t = + TyN + (G.Id + ((obj_name, G.fake obj_name), + G.empty_id_info ())); + t_attrs = []; + } + in + resolve_constructor_from_type ~lang ~all_funcs + ~imported_entity_index ?current_file ty + | None -> None))) | G.DotAccess ({ e = G.Call (constructor_callee, _); _ }, _, G.FN (G.Id ((id, _), _id_info))) -> ( @@ -920,6 +1042,10 @@ let identify_callee ?(object_mappings = []) ?(all_funcs = []) let class_name_str_opt = match constructor_callee.G.e with | G.N (G.Id ((class_name, _), _)) -> Some class_name + | G.N (G.IdQualified qualified_info) -> + AST_generic_helpers.dotted_ident_of_name + (G.IdQualified qualified_info) + |> List_.map fst |> List_.last_opt | _ -> (match dotted_name_segments_of_expr constructor_callee with | Some canonical -> List_.last_opt canonical @@ -927,6 +1053,22 @@ let identify_callee ?(object_mappings = []) ?(all_funcs = []) in Option.bind class_name_str_opt (fun class_name_str -> resolve_method_call_in_class class_name_str method_name_str)) + | G.DotAccess + ({ e = G.New (_, ty, _, _); _ }, _, G.FN (G.Id ((method_name, _), _))) + when Lang.(lang =*= Java || lang =*= Js || lang =*= Ts || lang =*= Csharp) + -> ( + let class_name_opt = + match ty.G.t with + | G.TyN (G.Id ((cn, _), _)) -> Some cn + | G.TyExpr { G.e = G.N (G.Id ((cn, _), _)); _ } -> Some cn + | G.TyExpr { G.e = G.N (G.IdQualified qualified_info); _ } -> + AST_generic_helpers.dotted_ident_of_name + (G.IdQualified qualified_info) + |> List_.map fst |> List_.last_opt + | _ -> None + in + Option.bind class_name_opt (fun class_name_str -> + resolve_method_call_in_class class_name_str method_name)) (* Method call: obj.method() - look up obj's class *) | _ -> Log.debug (fun m -> @@ -935,7 +1077,7 @@ let identify_callee ?(object_mappings = []) ?(all_funcs = []) None (* Extract all calls from a function body and resolve them to fn_ids *) -let extract_calls ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path = []) +let extract_calls ~(lang : Lang.t) ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path = []) ?(import_binding_timeline = StringMap.empty) ?(imported_entity_index = CanonicalMap.empty) ?(current_file : Fpath.t option) (fdef : G.function_definition) : (fn_id * Tok.t) list = @@ -957,7 +1099,7 @@ let extract_calls ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path | None -> (* Unresolved - try to identify it as a function *) (match - identify_callee ~object_mappings ~all_funcs + identify_callee ~lang ~object_mappings ~all_funcs ~import_binding_timeline ~local_import_binding_timeline ~imported_entity_index ?current_file ~caller_parent_path @@ -981,16 +1123,9 @@ let extract_calls ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path | G.Call (callee, args) -> let (_, args_list, _) = args in let call_arity = List.length args_list in - let tok = - match callsite_tok_of_callee_expr callee with - | Some tok -> tok - | None -> ( - match AST_generic_helpers.ii_of_any (G.E e) with - | tok :: _ -> tok - | [] -> Tok.unsafe_fake_tok "") - in + let tok = callsite_tok_for_call ~lang e callee in (match - identify_callee ~object_mappings ~all_funcs + identify_callee ~lang ~object_mappings ~all_funcs ~import_binding_timeline ~local_import_binding_timeline ~imported_entity_index ?current_file ~caller_parent_path @@ -1006,6 +1141,22 @@ let extract_calls ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path self#visit_expr env callee; (* Continue visiting arguments for nested calls *) super#visit_arguments env args + | G.New (_tok, ty, _id_info, args) -> + (* Constructor call: new ClassName(args). + Use the class name token so it matches the eorig token + in class_construction's constructor expression. *) + (match resolve_constructor_from_type ~lang ~all_funcs ty with + | Some fn_id -> + let tok = + match AST_generic_helpers.ii_of_any (G.T ty) with + | tok :: _ -> tok + | [] -> Tok.unsafe_fake_tok "" + in + calls := (fn_id, tok) :: !calls + | None -> ()); + let (_, args_list, _) = args in + List.iter check_arg_for_unresolved_function_call args_list; + super#visit_arguments env args | _ -> super#visit_expr env e end in @@ -1015,10 +1166,11 @@ let extract_calls ?(object_mappings = []) ?(all_funcs = []) ?(caller_parent_path (* Extract calls from top-level statements (outside any function). This returns a list of (callee_fn_id, call_tok) pairs. *) -let extract_toplevel_calls ?(object_mappings = []) ?(all_funcs = []) - ?(import_binding_timeline = StringMap.empty) - ?(imported_entity_index = CanonicalMap.empty) ?(current_file : Fpath.t option) - (ast : G.program) : (fn_id * Tok.t) list = +let extract_toplevel_calls ~(lang : Lang.t) ?(object_mappings = []) + ?(all_funcs = []) ?(import_binding_timeline = StringMap.empty) + ?(imported_entity_index = CanonicalMap.empty) + ?(current_file : Fpath.t option) (ast : G.program) : + (fn_id * Tok.t) list = Log.debug (fun m -> m "CALL_EXTRACT: Starting extraction for top-level statements"); let calls = ref [] in @@ -1060,16 +1212,9 @@ let extract_toplevel_calls ?(object_mappings = []) ?(all_funcs = []) in if call_pos >= 0 && not (is_inside_function call_pos) then ( (* Top-level call - no class context *) - let tok = - match callsite_tok_of_callee_expr callee with - | Some tok -> tok - | None -> ( - match AST_generic_helpers.ii_of_any (G.E e) with - | tok :: _ -> tok - | [] -> Tok.unsafe_fake_tok "") - in + let tok = callsite_tok_for_call ~lang e callee in match - identify_callee ~object_mappings ~all_funcs + identify_callee ~lang ~object_mappings ~all_funcs ~import_binding_timeline ~imported_entity_index ?current_file ~caller_parent_path:[] ~call_tok:tok ~import_lookup_scope:(TopLevelAtCallSite tok) @@ -1386,7 +1531,7 @@ let build_call_graph_with_context ~(lang : Lang.t) ?(object_mappings = []) (* Extract calls - class context is already in fn_id *) let callee_calls = - extract_calls ~object_mappings ~all_funcs:funcs + extract_calls ~lang ~object_mappings ~all_funcs:funcs ~import_binding_timeline ~imported_entity_index ?current_file ~caller_parent_path:fn_id fdef @@ -1432,7 +1577,7 @@ let build_call_graph_with_context ~(lang : Lang.t) ?(object_mappings = []) (* Extract calls from top-level code (outside any function) and add edges to *) let toplevel_calls = - extract_toplevel_calls ~object_mappings ~all_funcs:funcs + extract_toplevel_calls ~lang ~object_mappings ~all_funcs:funcs ~import_binding_timeline ~imported_entity_index ?current_file ast in diff --git a/tests/parsing/dockerfile/comment-in-continuation.dockerfile b/tests/parsing/dockerfile/comment-in-continuation.dockerfile new file mode 100644 index 0000000000..41b3b828a9 --- /dev/null +++ b/tests/parsing/dockerfile/comment-in-continuation.dockerfile @@ -0,0 +1,19 @@ +FROM alpine:3.19 + +# Comment inside a RUN continuation: the comment is transparent and the +# result is equivalent to "RUN echo hello world". +RUN echo hello \ +# comment between continuation lines + world + +# Multiple consecutive comments inside a continuation +RUN apt-get install -y \ +# first comment +# second comment + curl \ +# third comment + wget + +# Comment at the start, not inside a continuation (should parse as blank) +# regular comment here +RUN echo done diff --git a/tests/parsing/dockerfile/double-backslash-continuation.dockerfile b/tests/parsing/dockerfile/double-backslash-continuation.dockerfile new file mode 100644 index 0000000000..7d977bffe3 --- /dev/null +++ b/tests/parsing/dockerfile/double-backslash-continuation.dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.19 + +# Issue #629: double backslash at end of continuation line. +# Docker treats the last backslash as the continuation marker regardless +# of what precedes it, so "echo a \\ " still continues to the next line. +RUN echo a \\ + world + +# Same with trailing space after the double backslash +RUN echo hello \\ + world + +RUN apt-get install -y \\ + curl diff --git a/tests/parsing/dockerfile/escape-directive.dockerfile b/tests/parsing/dockerfile/escape-directive.dockerfile new file mode 100644 index 0000000000..8784e71faf --- /dev/null +++ b/tests/parsing/dockerfile/escape-directive.dockerfile @@ -0,0 +1,12 @@ +# escape=` +FROM alpine:3.19 + +# Issue #629: backtick as alternative escape/continuation character. +# The "# escape=`" directive tells Docker (and our preprocessor) to treat +# backtick as the line-continuation marker instead of backslash. +RUN echo hello ` + world + +RUN apt-get install -y ` + curl ` + wget diff --git a/tests/parsing/dockerfile/trailing-ws-continuation.dockerfile b/tests/parsing/dockerfile/trailing-ws-continuation.dockerfile new file mode 100644 index 0000000000..ed718653d3 --- /dev/null +++ b/tests/parsing/dockerfile/trailing-ws-continuation.dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.19 + +# Issue #629: trailing whitespace after line-continuation backslash +# "\ " (backslash + spaces) at end of line used to cause a +# Tok.NoTokenLocation crash in concat_shell_fragments. +RUN echo hello \ + world + +RUN apt-get install -y \ + curl \ + wget diff --git a/tests/patterns/dockerfile/continuation-comment.dockerfile b/tests/patterns/dockerfile/continuation-comment.dockerfile new file mode 100644 index 0000000000..39234843da --- /dev/null +++ b/tests/patterns/dockerfile/continuation-comment.dockerfile @@ -0,0 +1,14 @@ +FROM alpine:3.19 + +# Comment inside a continuation: the RUN instruction should still match +# and the match should be reported at the RUN line (location correctness). + +# MATCH: +RUN apt-get install -y \ +# this comment is transparent to the instruction + curl \ +# another transparent comment + wget + +# Not a match (different command) +RUN echo done diff --git a/tests/patterns/dockerfile/continuation-comment.sgrep b/tests/patterns/dockerfile/continuation-comment.sgrep new file mode 100644 index 0000000000..7d3f320572 --- /dev/null +++ b/tests/patterns/dockerfile/continuation-comment.sgrep @@ -0,0 +1 @@ +RUN apt-get install ... diff --git a/tests/patterns/dockerfile/continuation-escape-directive.dockerfile b/tests/patterns/dockerfile/continuation-escape-directive.dockerfile new file mode 100644 index 0000000000..dc795997d5 --- /dev/null +++ b/tests/patterns/dockerfile/continuation-escape-directive.dockerfile @@ -0,0 +1,14 @@ +# escape=` +FROM alpine:3.19 + +# Backtick as continuation character (Windows/PowerShell Dockerfiles). +# The match must be reported at the RUN line. + +# MATCH: +RUN echo hello ` + world + +# MATCH: +RUN echo foo ` + bar ` + baz diff --git a/tests/patterns/dockerfile/continuation-escape-directive.sgrep b/tests/patterns/dockerfile/continuation-escape-directive.sgrep new file mode 100644 index 0000000000..f52c48b9f7 --- /dev/null +++ b/tests/patterns/dockerfile/continuation-escape-directive.sgrep @@ -0,0 +1 @@ +RUN echo ... diff --git a/tests/patterns/dockerfile/continuation-trailing-ws.dockerfile b/tests/patterns/dockerfile/continuation-trailing-ws.dockerfile new file mode 100644 index 0000000000..3e8a64b2f3 --- /dev/null +++ b/tests/patterns/dockerfile/continuation-trailing-ws.dockerfile @@ -0,0 +1,16 @@ +FROM alpine:3.19 + +# Trailing whitespace after backslash continuation (issue #629). +# The match must be reported at the RUN line. + +# MATCH: +RUN echo hello \ + world + +# MATCH: +RUN echo a \\ + b + +# MATCH: +RUN echo foo \\ + bar diff --git a/tests/patterns/dockerfile/continuation-trailing-ws.sgrep b/tests/patterns/dockerfile/continuation-trailing-ws.sgrep new file mode 100644 index 0000000000..f52c48b9f7 --- /dev/null +++ b/tests/patterns/dockerfile/continuation-trailing-ws.sgrep @@ -0,0 +1 @@ +RUN echo ... diff --git a/tests/patterns/elixir/dots_arrow_map.ex b/tests/patterns/elixir/dots_arrow_map.ex new file mode 100644 index 0000000000..2a52cea8fe --- /dev/null +++ b/tests/patterns/elixir/dots_arrow_map.ex @@ -0,0 +1,20 @@ +# MATCH: no other elements +%{"some_item" => 0} + +# MATCH: 1 other element +%{"some_item" => 0, "some_other_item" => 1} + +# MATCH: 1 other element, before the target +%{"some_other_item" => 1, "some_item" => 0} + +# MATCH: 2 other elements +%{"some_item" => 0, "some_other_item" => 1, "yet_another" => 2} + +# MATCH: 2 other elements +%{"some_other_item" => 1, "some_item" => 0, "yet_another" => 2} + +# no match: keyword syntax, not arrow +%{some_item: 0} + +# no match: keyword syntax with other elements +%{some_item: 0, some_other_item: 1} diff --git a/tests/patterns/elixir/dots_arrow_map.sgrep b/tests/patterns/elixir/dots_arrow_map.sgrep new file mode 100644 index 0000000000..a406c0232e --- /dev/null +++ b/tests/patterns/elixir/dots_arrow_map.sgrep @@ -0,0 +1 @@ +%{..., "some_item" => $V, ...} diff --git a/tests/patterns/elixir/dots_kw_map.ex b/tests/patterns/elixir/dots_kw_map.ex index 330ec4d761..f8a2422e0d 100644 --- a/tests/patterns/elixir/dots_kw_map.ex +++ b/tests/patterns/elixir/dots_kw_map.ex @@ -12,3 +12,9 @@ # MATCH: 2 other elements %{some_other_item: 1, some_item: 0, yet_another_other_item: 2} + +# no match: arrow syntax, not keyword +%{"some_item" => 0} + +# no match: arrow syntax with other elements +%{"some_item" => 0, "some_other_item" => 1} diff --git a/tests/patterns/elixir/fun_forms.ex b/tests/patterns/elixir/fun_forms.ex index ba0a6d202a..0423ce5df5 100644 --- a/tests/patterns/elixir/fun_forms.ex +++ b/tests/patterns/elixir/fun_forms.ex @@ -1,9 +1,31 @@ #ERROR: -def foo() do +def foo1() do 42 end #ERROR: -def bar(), do: 42 +def foo2(), do: 42 +#ERROR: +def foo3 do + 42 +end + +#ERROR: +def foo4, do: 42 + +#ERROR: +def foo5() when x > 1 do + 42 +end + +#ERROR: +def foo6() when x > 1, do: 42 + +#ERROR: +def foo7 when x > 1 do + 42 +end +#ERROR: +def foo8 when x > 1, do: 42 diff --git a/tests/patterns/elixir/list-bar.ex b/tests/patterns/elixir/list-bar.ex new file mode 100644 index 0000000000..ab7acff26d --- /dev/null +++ b/tests/patterns/elixir/list-bar.ex @@ -0,0 +1,7 @@ +def foo() do + # ERROR: + [1,2|3] + + # OK: + [1,2,3] +end diff --git a/tests/patterns/elixir/list-bar.sgrep b/tests/patterns/elixir/list-bar.sgrep new file mode 100644 index 0000000000..37366e7b02 --- /dev/null +++ b/tests/patterns/elixir/list-bar.sgrep @@ -0,0 +1 @@ +[$X,$Y|$Z] diff --git a/tests/patterns/elixir/list-elements.ex b/tests/patterns/elixir/list-elements.ex new file mode 100644 index 0000000000..c0826c28c1 --- /dev/null +++ b/tests/patterns/elixir/list-elements.ex @@ -0,0 +1,7 @@ +def foo() do + # ERROR: + [1,2,3] + + # OK: + [1,2|3] +end diff --git a/tests/patterns/elixir/list-elements.sgrep b/tests/patterns/elixir/list-elements.sgrep new file mode 100644 index 0000000000..6222244e66 --- /dev/null +++ b/tests/patterns/elixir/list-elements.sgrep @@ -0,0 +1 @@ +[$X,$Y,$Z] diff --git a/tests/patterns/elixir/rescue_when.ex b/tests/patterns/elixir/rescue_when.ex new file mode 100644 index 0000000000..06a8e00c13 --- /dev/null +++ b/tests/patterns/elixir/rescue_when.ex @@ -0,0 +1,6 @@ +def guard_sink(x) do + dangerous() +catch + # ERROR: + _kind, _value when test() -> :ok +end diff --git a/tests/patterns/elixir/rescue_when.sgrep b/tests/patterns/elixir/rescue_when.sgrep new file mode 100644 index 0000000000..8be63798f7 --- /dev/null +++ b/tests/patterns/elixir/rescue_when.sgrep @@ -0,0 +1 @@ +test() diff --git a/tests/patterns/ruby/array_access.rb b/tests/patterns/ruby/array_access.rb new file mode 100644 index 0000000000..fa46cb9760 --- /dev/null +++ b/tests/patterns/ruby/array_access.rb @@ -0,0 +1,10 @@ +# params[...] should match hash access on bare identifiers (method calls) +#ERROR: match +params[:id] + +#ERROR: match +sink(params[:to]) + +# Also match explicit call form +#ERROR: match +params()[:name] diff --git a/tests/patterns/ruby/array_access.sgrep b/tests/patterns/ruby/array_access.sgrep new file mode 100644 index 0000000000..7d5fca4bb9 --- /dev/null +++ b/tests/patterns/ruby/array_access.sgrep @@ -0,0 +1 @@ +params[...] \ No newline at end of file diff --git a/tests/patterns/ruby/array_access_call.rb b/tests/patterns/ruby/array_access_call.rb new file mode 100644 index 0000000000..d83583c205 --- /dev/null +++ b/tests/patterns/ruby/array_access_call.rb @@ -0,0 +1,13 @@ +# params()[...] should match hash access on method calls +#ERROR: match +params[:id] + +#ERROR: match +sink(params[:to]) + +#ERROR: match +params()[:name] + +# Local variable should NOT match params()[...] +params = { id: 1 } +x = params[:id] diff --git a/tests/patterns/ruby/array_access_call.sgrep b/tests/patterns/ruby/array_access_call.sgrep new file mode 100644 index 0000000000..96858a27d4 --- /dev/null +++ b/tests/patterns/ruby/array_access_call.sgrep @@ -0,0 +1 @@ +params()[...] \ No newline at end of file diff --git a/tests/patterns/ruby/misc_hidden_call.rb b/tests/patterns/ruby/misc_hidden_call.rb index c910a02c9d..6c2afeef2d 100644 --- a/tests/patterns/ruby/misc_hidden_call.rb +++ b/tests/patterns/ruby/misc_hidden_call.rb @@ -1,12 +1,21 @@ #ERROR: match foo(1,2,3) -# an identifier on its own as a statement is probably an hidden funcall -# which is why ruby_to_generic.ml convert that in a Call +# A bare identifier with no prior assignment is a method call in Ruby. +# Disambiguate_ruby_calls wraps it in Call() after naming. #ERROR: match foo a = bar -# In Ruby, a bare identifier with no prior assignment is a method call. #ERROR: match b = foo + +# A bare identifier WITH a prior assignment is a local variable, +# not a method call. It should NOT match foo(...). +foo = 42 +c = foo + +# A parameter is also not a method call. +def use_param(foo) + d = foo +end diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs.rb b/tests/rules/cross_function_tainting/test_constructor_taint_bugs.rb new file mode 100644 index 0000000000..34ca8bbdf8 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs.rb @@ -0,0 +1,38 @@ +# FALSE NEGATIVE: zero-arg constructor with internal source. +# The taint engine never analyzes initialize, so @data is not tainted. +class InternalSource + def initialize + @data = source() + end + + def get_data + @data + end +end + +def test_false_negative + obj = InternalSource.new() + result = obj.get_data() + # ruleid: constructor-taint-bugs + sink(result) +end + +# FALSE POSITIVE: constructor ignores its argument. +# The taint engine leaks source() through all_args_taints +# even though initialize never stores it. +class IgnoresArg + def initialize(data) + @data = "safe" + end + + def get_data + @data + end +end + +def test_false_positive + obj = IgnoresArg.new(source()) + result = obj.get_data() + # ok: constructor-taint-bugs + sink(result) +end diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs.yaml b/tests/rules/cross_function_tainting/test_constructor_taint_bugs.yaml new file mode 100644 index 0000000000..40ac893ec3 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs.yaml @@ -0,0 +1,10 @@ +rules: + - id: constructor-taint-bugs + message: Taint through constructor + languages: [ruby] + severity: ERROR + mode: taint + pattern-sources: + - pattern: source() + pattern-sinks: + - pattern: sink(...) diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.java b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.java new file mode 100644 index 0000000000..933cbb5da6 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.java @@ -0,0 +1,41 @@ +// FALSE NEGATIVE: zero-arg constructor with internal source. +class InternalSource { + String data; + + InternalSource() { + this.data = source(); + } + + String getData() { + return this.data; + } +} + +// FALSE POSITIVE: constructor ignores its argument. +class IgnoresArg { + String data; + + IgnoresArg(String input) { + this.data = "safe"; + } + + String getData() { + return this.data; + } +} + +class Test { + void testFalseNegative() { + InternalSource obj = new InternalSource(); + String result = obj.getData(); + // ruleid: constructor-taint-bugs + sink(result); + } + + void testFalsePositive() { + IgnoresArg obj = new IgnoresArg(source()); + String result = obj.getData(); + // ok: constructor-taint-bugs + sink(result); + } +} diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.yaml b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.yaml new file mode 100644 index 0000000000..59e571305d --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_java.yaml @@ -0,0 +1,10 @@ +rules: + - id: constructor-taint-bugs + message: Taint through constructor + languages: [java] + severity: ERROR + mode: taint + pattern-sources: + - pattern: source() + pattern-sinks: + - pattern: sink(...) diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.js b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.js new file mode 100644 index 0000000000..38a5064dd5 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.js @@ -0,0 +1,35 @@ +// FALSE NEGATIVE: zero-arg constructor with internal source. +class InternalSource { + constructor() { + this.data = source(); + } + + getData() { + return this.data; + } +} + +// FALSE POSITIVE: constructor ignores its argument. +class IgnoresArg { + constructor(data) { + this.data = "safe"; + } + + getData() { + return this.data; + } +} + +function testFalseNegative() { + let obj = new InternalSource(); + let result = obj.getData(); + // ruleid: constructor-taint-bugs + sink(result); +} + +function testFalsePositive() { + let obj = new IgnoresArg(source()); + let result = obj.getData(); + // ok: constructor-taint-bugs + sink(result); +} diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.yaml b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.yaml new file mode 100644 index 0000000000..5a563c4759 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_js.yaml @@ -0,0 +1,10 @@ +rules: + - id: constructor-taint-bugs + message: Taint through constructor + languages: [javascript] + severity: ERROR + mode: taint + pattern-sources: + - pattern: source() + pattern-sinks: + - pattern: sink(...) diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.py b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.py new file mode 100644 index 0000000000..65010accfe --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.py @@ -0,0 +1,27 @@ +# FALSE NEGATIVE: zero-arg constructor with internal source. +class InternalSource: + def __init__(self): + self.data = source() + + def get_data(self): + return self.data + +def test_false_negative(): + obj = InternalSource() + result = obj.get_data() + # ruleid: constructor-taint-bugs + sink(result) + +# FALSE POSITIVE: constructor ignores its argument. +class IgnoresArg: + def __init__(self, data): + self.data = "safe" + + def get_data(self): + return self.data + +def test_false_positive(): + obj = IgnoresArg(source()) + result = obj.get_data() + # ok: constructor-taint-bugs + sink(result) diff --git a/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.yaml b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.yaml new file mode 100644 index 0000000000..040e7888e0 --- /dev/null +++ b/tests/rules/cross_function_tainting/test_constructor_taint_bugs_python.yaml @@ -0,0 +1,10 @@ +rules: + - id: constructor-taint-bugs + message: Taint through constructor + languages: [python] + severity: ERROR + mode: taint + pattern-sources: + - pattern: source() + pattern-sinks: + - pattern: sink(...) diff --git a/tests/rules/cross_function_tainting/test_csharp_constructor.cs b/tests/rules/cross_function_tainting/test_csharp_constructor.cs index 0f434da096..dca8a33b8c 100644 --- a/tests/rules/cross_function_tainting/test_csharp_constructor.cs +++ b/tests/rules/cross_function_tainting/test_csharp_constructor.cs @@ -62,7 +62,11 @@ public static void Main() FieldUser fieldUser = new FieldUser(); fieldUser.name = taintedInput2; string fieldResult = fieldUser.GetProfile(); - + + // Test chained method call: new Constructor(tainted).method() + // ruleid: csharp_constructor_sqli + string chainedResult = "SELECT * FROM users WHERE name = " + new User(source()).GetProfile(); + return; } } \ No newline at end of file diff --git a/tests/rules/cross_function_tainting/test_java_constructor.java b/tests/rules/cross_function_tainting/test_java_constructor.java index e2ee47b313..5bf24aaa7f 100644 --- a/tests/rules/cross_function_tainting/test_java_constructor.java +++ b/tests/rules/cross_function_tainting/test_java_constructor.java @@ -68,6 +68,10 @@ public static void main(String[] args) { IntermethodClass intermethodObj = new IntermethodClass(); String intermethodResult = intermethodObj.sinkMethod(); + // Test chained method call: new Constructor(tainted).method() + // ruleid: java_constructor_sqli + String chainedResult = "SELECT * FROM users WHERE name = " + new User(source()).getProfile(); + return; } } \ No newline at end of file diff --git a/tests/rules/cross_function_tainting/test_javascript_constructor.js b/tests/rules/cross_function_tainting/test_javascript_constructor.js index 8b67431318..1f672c0a27 100644 --- a/tests/rules/cross_function_tainting/test_javascript_constructor.js +++ b/tests/rules/cross_function_tainting/test_javascript_constructor.js @@ -63,5 +63,9 @@ function main() { // ruleid: javascript_constructor_sqli sink(a); + // Test chained method call: new Constructor(tainted).method() + // ruleid: javascript_constructor_sqli + sink(new User(source()).getProfile()); + return result; } diff --git a/tests/rules/cross_function_tainting/test_kotlin_constructor.kt b/tests/rules/cross_function_tainting/test_kotlin_constructor.kt index f180bebc90..a2c6071136 100644 --- a/tests/rules/cross_function_tainting/test_kotlin_constructor.kt +++ b/tests/rules/cross_function_tainting/test_kotlin_constructor.kt @@ -67,5 +67,9 @@ fun main() { val intermethodObj = IntermethodClass() val intermethodResult = intermethodObj.sinkMethod() + // Test chained method call: Constructor(tainted).method() + // ruleid: kotlin_constructor_sqli + sink(User(source()).getProfile()) + return } diff --git a/tests/rules/cross_function_tainting/test_python_constructor.py b/tests/rules/cross_function_tainting/test_python_constructor.py index 22a423397d..45906d6d10 100644 --- a/tests/rules/cross_function_tainting/test_python_constructor.py +++ b/tests/rules/cross_function_tainting/test_python_constructor.py @@ -20,7 +20,7 @@ def intermediateFun (): tainted_input = source() user = User(tainted_input) return user -def sink_ex(user): +def sink_ex(user : User): return user.get_profile() class FieldUser: def __init__(self): @@ -54,4 +54,8 @@ def main(): intermethod_obj = IntermethodClass() intermethod_result = intermethod_obj.sink_method() + # Test chained method call: Constructor(tainted).method() + # ruleid:python_constructor_sqli + chained_result = f"SELECT * FROM users WHERE name = {User(source()).get_profile()}" + return result diff --git a/tests/rules/cross_function_tainting/test_ruby_array_access.rb b/tests/rules/cross_function_tainting/test_ruby_array_access.rb new file mode 100644 index 0000000000..7ecc4b820f --- /dev/null +++ b/tests/rules/cross_function_tainting/test_ruby_array_access.rb @@ -0,0 +1,24 @@ +# Test that hash/array access `obj[:key]` propagates taint correctly. +# `obj[:key]` was misparsed as Call(DotAccess(obj, Op_AREF), [:key]) +# instead of ArrayAccess, which broke taint propagation because +# a Call to `[]` has no signature, while ArrayAccess preserves taint. + +class TestController + def show + if continue_params[:to] + # ruleid: test-ruby-array-access + sink(continue_params[:to]) + end + end + + def continue_params + source() + end +end + +def source() + "tainted" +end + +def sink(x) +end diff --git a/tests/rules/cross_function_tainting/test_ruby_array_access.yaml b/tests/rules/cross_function_tainting/test_ruby_array_access.yaml new file mode 100644 index 0000000000..c6b20eaccc --- /dev/null +++ b/tests/rules/cross_function_tainting/test_ruby_array_access.yaml @@ -0,0 +1,13 @@ +rules: + - id: test-ruby-array-access + message: Test hash access does not break taint + languages: + - ruby + severity: WARNING + mode: taint + options: + taint_intrafile: true + pattern-sources: + - pattern: source(...) + pattern-sinks: + - pattern: sink(...) diff --git a/tests/rules/cross_function_tainting/test_ruby_constructor.rb b/tests/rules/cross_function_tainting/test_ruby_constructor.rb index dc0b26ddaf..9769c3f6b7 100644 --- a/tests/rules/cross_function_tainting/test_ruby_constructor.rb +++ b/tests/rules/cross_function_tainting/test_ruby_constructor.rb @@ -31,5 +31,9 @@ def main intermethod_obj = IntermethodClass.new() intermethod_result = intermethod_obj.sink_method() + # Test chained method call: ClassName.new(tainted).method() + # ruleid: ruby_constructor_sqli + chained_result = "SELECT * FROM users WHERE name = #{User.new(taint).get_profile()}" + return result end diff --git a/tests/rules/cross_function_tainting/test_ruby_zero_arg_dispatch.rb b/tests/rules/cross_function_tainting/test_ruby_zero_arg_dispatch.rb index cd54ad2c6f..013b65a925 100644 --- a/tests/rules/cross_function_tainting/test_ruby_zero_arg_dispatch.rb +++ b/tests/rules/cross_function_tainting/test_ruby_zero_arg_dispatch.rb @@ -105,13 +105,13 @@ def get_data def test_zero_arg_new_with_parens obj = TaintedService.new() result = obj.get_data() - # todoruleid: test-ruby-zero-arg-dispatch + # ruleid: test-ruby-zero-arg-dispatch sink(result) end def test_zero_arg_new_no_parens obj = TaintedService.new result = obj.get_data() - # todoruleid: test-ruby-zero-arg-dispatch + # ruleid: test-ruby-zero-arg-dispatch sink(result) end diff --git a/tests/rules/cross_function_tainting/test_scala_constructor.scala b/tests/rules/cross_function_tainting/test_scala_constructor.scala index 0fbe3d5833..897629716d 100644 --- a/tests/rules/cross_function_tainting/test_scala_constructor.scala +++ b/tests/rules/cross_function_tainting/test_scala_constructor.scala @@ -41,7 +41,11 @@ object Main { val fieldUser = new FieldUser() fieldUser.name = taintedInput2 val fieldResult = fieldUser.getProfile() - + + // Test chained method call: new Constructor(tainted).method() + // ruleid: scala_constructor_sqli + sink(new User(source()).getProfile()) + return } } diff --git a/tests/rules/cross_function_tainting/test_typescript_constructor.ts b/tests/rules/cross_function_tainting/test_typescript_constructor.ts index bf89f4021f..750a330c0b 100644 --- a/tests/rules/cross_function_tainting/test_typescript_constructor.ts +++ b/tests/rules/cross_function_tainting/test_typescript_constructor.ts @@ -64,5 +64,9 @@ function main(): void { // ruleid: typescript_constructor_sqli const query: string = `SELECT * FROM users WHERE name = ${a}`; + // Test chained method call: new Constructor(tainted).method() + // ruleid: typescript_constructor_sqli + const chainedQuery: string = `SELECT * FROM users WHERE name = ${new User(source()).getProfile()}`; + return; } \ No newline at end of file diff --git a/tests/tainting/java_collection_models.java b/tests/tainting/java_collection_models.java index 510c826981..95ba216a2e 100644 --- a/tests/tainting/java_collection_models.java +++ b/tests/tainting/java_collection_models.java @@ -3,7 +3,9 @@ import java.util.HashMap; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; public class CollectionModelsTest { @@ -51,7 +53,53 @@ public void testClean() { sink(value); } + // Test that getter-style lookups preserve taint through deep chains + public void testDeepGetterChain(String tainted, Foo foo) { + String value = foo.getBars(tainted).get(0).getX(); + // ruleid: java-collection-taint + sink(value); + } + + // Test that getter-style lookups preserve taint through chained fields + public void testDeepFieldChain(String tainted, SiteModel siteModel) { + Integer value = siteModel.getPrefixes(tainted).sites.ids.get(0); + // ruleid: java-collection-taint + sink(String.valueOf(value)); + } + private void sink(String data) { System.out.println(data); } } + +class Bar { + String x; + + String getX() { + return x; + } +} + +class Foo { + List bars; + + List getBars(String name) { + return bars; + } +} + +class SiteModel { + PrefixSiteIds prefixes; + + PrefixSiteIds getPrefixes(String name) { + return prefixes; + } +} + +class PrefixSiteIds { + SiteIds sites; +} + +class SiteIds { + Set ids = new HashSet<>(); +} diff --git a/tests/tainting_rules/elixir/taint-cond-case.ex b/tests/tainting_rules/elixir/taint-cond-case.ex new file mode 100644 index 0000000000..7fe4aba8dc --- /dev/null +++ b/tests/tainting_rules/elixir/taint-cond-case.ex @@ -0,0 +1,7 @@ +def foo() do + x = source() + cond do + # ruleid: cond_taint_rule + y = sink(x) -> 3 + end +end diff --git a/tests/tainting_rules/elixir/taint-cond-case.yaml b/tests/tainting_rules/elixir/taint-cond-case.yaml new file mode 100644 index 0000000000..0ce2b8bd9a --- /dev/null +++ b/tests/tainting_rules/elixir/taint-cond-case.yaml @@ -0,0 +1,12 @@ +rules: +- id: cond_taint_rule + languages: [elixir] + message: "tainted data reached sink" + severity: ERROR + mode: taint + pattern-sources: + - pattern: | + source() + pattern-sinks: + - pattern: | + sink(...) diff --git a/tests/tainting_rules/elixir/taint-for-comprehension.ex b/tests/tainting_rules/elixir/taint-for-comprehension.ex new file mode 100644 index 0000000000..5f4851cef8 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-for-comprehension.ex @@ -0,0 +1,32 @@ +def basic(data) do + for x <- data do + # ruleid: for_comp_taint + sink(x) + end +end + +def with_pipe(data) do + for line <- data do + # ruleid: for_comp_taint + line + |> Base.decode64!() + |> sink() + end +end + +def multiple_generators(xs, ys) do + for x <- xs, y <- ys do + # ruleid: for_comp_taint + sink(x) + # ruleid: for_comp_taint + sink(y) + end +end + +def no_taint(data) do + other = "safe" + for x <- data do + # ok: for_comp_taint + sink(other) + end +end diff --git a/tests/tainting_rules/elixir/taint-for-comprehension.yaml b/tests/tainting_rules/elixir/taint-for-comprehension.yaml new file mode 100644 index 0000000000..22ed6eb521 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-for-comprehension.yaml @@ -0,0 +1,17 @@ +rules: +- id: for_comp_taint + languages: [elixir] + message: "tainted data reached sink via for comprehension" + severity: ERROR + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink(...) diff --git a/tests/tainting_rules/elixir/taint-list.ex b/tests/tainting_rules/elixir/taint-list.ex new file mode 100644 index 0000000000..063f3f8df9 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-list.ex @@ -0,0 +1,15 @@ +def foo([x,y]) do + # ruleid: list_taint_rule + sink(x) + + # ruleid: list_taint_rule + sink(y) +end + +def bar([x|y]) do + # ruleid: list_taint_rule + sink(x) + + # ruleid: list_taint_rule + sink(y) +end diff --git a/tests/tainting_rules/elixir/taint-list.yaml b/tests/tainting_rules/elixir/taint-list.yaml new file mode 100644 index 0000000000..d54d84b279 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-list.yaml @@ -0,0 +1,17 @@ +rules: +- id: list_taint_rule + languages: [elixir] + message: "tainted data reached sink" + severity: ERROR + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink(...) diff --git a/tests/tainting_rules/elixir/taint-map-param.ex b/tests/tainting_rules/elixir/taint-map-param.ex new file mode 100644 index 0000000000..d7fa128a2a --- /dev/null +++ b/tests/tainting_rules/elixir/taint-map-param.ex @@ -0,0 +1,48 @@ +defmodule TaintMapParam do + # keyword syntax, single clause + def keyword_single(%{user: user}) do + # ruleid: taint-map-param + sink(user) + end + + # arrow syntax, single clause + def arrow_single(%{"user" => user}) do + # ruleid: taint-map-param + sink(user) + end + + # multi-clause, keyword + def multi_kw(%{a: a}) do + # ruleid: taint-map-param + sink(a) + end + + def multi_kw(%{b: b}) do + # ruleid: taint-map-param + sink(b) + end + + # multi-clause, arrow + def multi_arrow(%{"a" => a}) do + # ruleid: taint-map-param + sink(a) + end + + def multi_arrow(%{"b" => b}) do + # ruleid: taint-map-param + sink(b) + end + + # plain param (not map), single clause + def plain(input) do + # ruleid: taint-map-param + sink(input) + end + + # safe: variable not from param + def safe(%{user: _user}) do + other = "safe" + # ok: taint-map-param + sink(other) + end +end diff --git a/tests/tainting_rules/elixir/taint-map-param.yaml b/tests/tainting_rules/elixir/taint-map-param.yaml new file mode 100644 index 0000000000..39cdc446ef --- /dev/null +++ b/tests/tainting_rules/elixir/taint-map-param.yaml @@ -0,0 +1,27 @@ +rules: +- id: taint-map-param + mode: taint + languages: [elixir] + message: "tainted via map param" + severity: INFO + + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - pattern-inside: | + def $_(..., %{..., $_: $X, ...}, ...) do + ... + end + - pattern-inside: | + def $_(..., %{..., $_ => $X, ...}, ...) do + ... + end + - focus-metavariable: $X + + pattern-sinks: + - pattern: | + sink($R) diff --git a/tests/tainting_rules/elixir/taint-map-value.ex b/tests/tainting_rules/elixir/taint-map-value.ex new file mode 100644 index 0000000000..9c4e3dc7df --- /dev/null +++ b/tests/tainting_rules/elixir/taint-map-value.ex @@ -0,0 +1,39 @@ +defmodule Taint.MapValue do + # taint flows through arrow map value expression + def transform_arrow(items) do + items + # ruleid: taint-map-value + |> Enum.map(fn {k, v} -> %{sink(k) => v} end) + end + + # taint flows through complex key expression in arrow map + def transform_arrow_key(items) do + items + # ruleid: taint-map-value + |> Enum.map(fn {k, v} -> %{String.downcase(sink(k)) => v} end) + end + + # taint flows through keyword map value expression + def transform_keyword(items) do + items + # ruleid: taint-map-value + |> Enum.map(fn {k, v} -> %{result: sink(k)} end) + end + + # taint through a direct call, not inside a map + def transform_direct(items) do + items + # ruleid: taint-map-value + |> Enum.map(fn {k, v} -> sink(k) end) + end + + # safe: variable not from param + def no_taint(items) do + items + |> Enum.map(fn {_k, _v} -> + other = "safe" + # ok: taint-map-value + sink(other) + end) + end +end diff --git a/tests/tainting_rules/elixir/taint-map-value.yaml b/tests/tainting_rules/elixir/taint-map-value.yaml new file mode 100644 index 0000000000..2f9b665cd7 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-map-value.yaml @@ -0,0 +1,21 @@ +rules: +- id: taint-map-value + mode: taint + languages: [elixir] + message: "tainted via map value" + severity: INFO + + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + fn ..., $X, ... -> ... end + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + + pattern-sinks: + - pattern: | + sink($R) diff --git a/tests/tainting_rules/elixir/taint-param-with-default.ex b/tests/tainting_rules/elixir/taint-param-with-default.ex new file mode 100644 index 0000000000..98ff427e2f --- /dev/null +++ b/tests/tainting_rules/elixir/taint-param-with-default.ex @@ -0,0 +1,4 @@ +def foo(x \\ "default value") do + # ruleid: taint + sink(x) +end diff --git a/tests/tainting_rules/elixir/taint-param-with-default.yaml b/tests/tainting_rules/elixir/taint-param-with-default.yaml new file mode 100644 index 0000000000..58eb1c4c68 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-param-with-default.yaml @@ -0,0 +1,17 @@ +rules: +- id: taint + languages: [elixir] + message: "tainted data reached sink" + severity: ERROR + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink(...) diff --git a/tests/tainting_rules/elixir/taint-pattern-as.ex b/tests/tainting_rules/elixir/taint-pattern-as.ex new file mode 100644 index 0000000000..81a2aef666 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-pattern-as.ex @@ -0,0 +1,7 @@ +def handle(%User{name: name} = user) do + #ruleid: taint-pattern-as + sink(name) + + #ruleid: taint-pattern-as + sink(user) +end diff --git a/tests/tainting_rules/elixir/taint-pattern-as.yaml b/tests/tainting_rules/elixir/taint-pattern-as.yaml new file mode 100644 index 0000000000..a7d8ce7d7b --- /dev/null +++ b/tests/tainting_rules/elixir/taint-pattern-as.yaml @@ -0,0 +1,20 @@ +rules: + - id: taint-pattern-as + message: taint + severity: WARNING + languages: + - elixir + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - patterns: + - focus-metavariable: $X + - pattern-either: + - pattern: sink($X) diff --git a/tests/tainting_rules/elixir/taint-pipe.ex b/tests/tainting_rules/elixir/taint-pipe.ex new file mode 100644 index 0000000000..17587444d3 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-pipe.ex @@ -0,0 +1,22 @@ +def single_pipe(data) do + # ruleid: pipe_taint + data |> sink() +end + +def chain(data) do + # ruleid: pipe_taint + data + |> decode() + |> sink() +end + +def pipe_with_args(data) do + # ruleid: pipe_taint + data |> sink(:option) +end + +def no_taint(data) do + other = "safe" + # ok: pipe_taint + other |> sink() +end diff --git a/tests/tainting_rules/elixir/taint-pipe.yaml b/tests/tainting_rules/elixir/taint-pipe.yaml new file mode 100644 index 0000000000..f40782d2ff --- /dev/null +++ b/tests/tainting_rules/elixir/taint-pipe.yaml @@ -0,0 +1,17 @@ +rules: +- id: pipe_taint + languages: [elixir] + message: "tainted data reached sink via pipe" + severity: ERROR + mode: taint + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink(...) diff --git a/tests/tainting_rules/elixir/taint-rescue.ex b/tests/tainting_rules/elixir/taint-rescue.ex new file mode 100644 index 0000000000..b9645c7801 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-rescue.ex @@ -0,0 +1,66 @@ +defmodule TaintRescue do + # taint flows through main body even when rescue clause is present + def with_rescue(x) do + # ruleid: taint-rescue + sink(x) + rescue + ArgumentError -> :error + end + + # guarded function with rescue + def guarded_rescue(x) when is_binary(x) do + # ruleid: taint-rescue + sink(x) + rescue + ArgumentError -> :error + end + + # taint also flows into the rescue handler body (x is still in scope) + def rescue_handler(x) do + some_call(x) + rescue + _e -> + # ruleid: taint-rescue + sink(x) + end + + # taint flows into the else clause (x is still in scope) + def with_else(x) do + some_call(x) + else + _result -> + # ruleid: taint-rescue + sink(x) + end + + # taint flows into the after clause (x is still in scope) + def with_after(x) do + some_call(x) + rescue + _e -> :error + after + # ruleid: taint-rescue + sink(x) + end + + # taint flows through body with all clauses present + def with_all_clauses(x) do + # ruleid: taint-rescue + sink(x) + rescue + _e -> :error + else + _result -> :ok + after + some_call(x) + end + + # taint flows into catch clause body (x is still in scope) + def with_catch(x) do + some_call(x) + catch + _kind, _value -> + # ruleid: taint-rescue + sink(x) + end +end diff --git a/tests/tainting_rules/elixir/taint-rescue.yaml b/tests/tainting_rules/elixir/taint-rescue.yaml new file mode 100644 index 0000000000..895282bab6 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-rescue.yaml @@ -0,0 +1,16 @@ +rules: +- id: taint-rescue + mode: taint + languages: [elixir] + message: "tainted in function with rescue clause" + severity: INFO + pattern-sources: + - patterns: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink($R) diff --git a/tests/tainting_rules/elixir/taint-struct-param.ex b/tests/tainting_rules/elixir/taint-struct-param.ex new file mode 100644 index 0000000000..677c00b785 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-struct-param.ex @@ -0,0 +1,49 @@ + +defmodule TaintStructParam do + # keyword syntax, single clause + def keyword_single(%Some_struct{user: user}) do + # ruleid: taint-struct-param + sink(user) + end + + # arrow syntax, single clause + def arrow_single(%Some_struct{"user" => user}) do + # ruleid: taint-struct-param + sink(user) + end + + # multi-clause, keyword + def multi_kw(%Some_struct{a: a}) do + # ruleid: taint-struct-param + sink(a) + end + + def multi_kw(%Some_struct{b: b}) do + # ruleid: taint-struct-param + sink(b) + end + + # multi-clause, arrow + def multi_arrow(%Some_struct{"a" => a}) do + # ruleid: taint-struct-param + sink(a) + end + + def multi_arrow(%Some_struct{"b" => b}) do + # ruleid: taint-struct-param + sink(b) + end + + # plain param (not map), single clause + def plain(input) do + # ruleid: taint-struct-param + sink(input) + end + + # safe: variable not from param + def safe(%Some_struct{user: _user}) do + other = "safe" + # ok: taint-struct-param + sink(other) + end +end diff --git a/tests/tainting_rules/elixir/taint-struct-param.yaml b/tests/tainting_rules/elixir/taint-struct-param.yaml new file mode 100644 index 0000000000..dfd22fa535 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-struct-param.yaml @@ -0,0 +1,27 @@ +rules: +- id: taint-struct-param + mode: taint + languages: [elixir] + message: "tainted via struct param" + severity: INFO + + pattern-sources: + - patterns: + - pattern-either: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - pattern-inside: | + def $_(..., %$_{..., $_: $X, ...}, ...) do + ... + end + - pattern-inside: | + def $_(..., %$_{..., $_ => $X, ...}, ...) do + ... + end + - focus-metavariable: $X + + pattern-sinks: + - pattern: | + sink($R) diff --git a/tests/tainting_rules/elixir/taint-try.ex b/tests/tainting_rules/elixir/taint-try.ex new file mode 100644 index 0000000000..3f2389a581 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-try.ex @@ -0,0 +1,72 @@ +defmodule TaintTry do + # taint flows through try body + def with_try(x) do + try do + # ruleid: taint-try + sink(x) + rescue + ArgumentError -> :error + end + end + + # taint flows into the rescue handler body (x is still in scope) + def try_rescue_handler(x) do + try do + some_call(x) + rescue + _e -> + # ruleid: taint-try + sink(x) + end + end + + # taint flows into the catch clause body (x is still in scope) + def try_catch_handler(x) do + try do + some_call(x) + catch + _kind, _value -> + # ruleid: taint-try + sink(x) + end + end + + # taint flows into the after clause (x is still in scope) + def try_with_after(x) do + try do + some_call(x) + rescue + _e -> :error + after + # ruleid: taint-try + sink(x) + end + end + + # taint flows into the else clause (x is still in scope) + def try_with_else(x) do + try do + some_call(x) + else + _result -> + # ruleid: taint-try + sink(x) + end + end + + # taint flows through body with all clauses present + def try_with_all_clauses(x) do + try do + # ruleid: taint-try + sink(x) + rescue + _e -> :error + catch + _kind, _value -> :caught + else + _result -> :ok + after + some_call(x) + end + end +end diff --git a/tests/tainting_rules/elixir/taint-try.yaml b/tests/tainting_rules/elixir/taint-try.yaml new file mode 100644 index 0000000000..c8f6dae994 --- /dev/null +++ b/tests/tainting_rules/elixir/taint-try.yaml @@ -0,0 +1,16 @@ +rules: +- id: taint-try + mode: taint + languages: [elixir] + message: "tainted value flows through standalone try block" + severity: INFO + pattern-sources: + - patterns: + - pattern-inside: | + def $_(..., $X, ...) do + ... + end + - focus-metavariable: $X + pattern-sinks: + - pattern: | + sink($R) diff --git a/tests/tainting_rules/python/comprehension.py b/tests/tainting_rules/python/comprehension.py index a707dccc7b..291dc70027 100644 --- a/tests/tainting_rules/python/comprehension.py +++ b/tests/tainting_rules/python/comprehension.py @@ -47,3 +47,48 @@ sanitized_col = [sanitize(x) for x in s] # ok: taint sink(sanitized_col) + + +## multiple generators: taint flows from each collection + +multi = [x + y for x in s for y in other] +# ruleid: taint +sink(multi) + +multi2 = [x + y for x in safe for y in s] +# ruleid: taint +sink(multi2) + + +## multiple generators: taint flows through iteration variable + +# ruleid: taint +res_multi = [sink(x) for x in s for y in other] +do_sth(res_multi) + + +## comprehension with filter: taint flows through + +filtered = [x for x in s if x > 0] +# ruleid: taint +sink(filtered) + + +## multiple generators: taint propagated to result's elements + +res_multi2 = [x + y for x in s for y in other] +for item in res_multi2: + # ruleid: taint + sink(item) + +res_multi3 = [x + y for x in safe for y in s] +for item in res_multi3: + # ruleid: taint + sink(item) + + +## multiple generators: no taint from clean collections + +clean_multi = [x + y for x in safe1 for y in safe2] +# ok: taint +sink(clean_multi)