From 6d8ee3345c364dc09f459afe6b3f3f54a653d937 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 24 Sep 2026 22:16:25 +0200 Subject: [PATCH 1/2] test: specify that SSRF sinks read the destination of a request only --- tests/test_cache.py | 11 ++- tests/test_interprocedural.py | 9 ++- tests/test_ssrf_targets.py | 124 ++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 tests/test_ssrf_targets.py diff --git a/tests/test_cache.py b/tests/test_cache.py index ad9db2d..f73c9be 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -86,7 +86,16 @@ def test_findings_and_summaries_round_trip_through_json() -> None: "f", 2, frozenset({0}), - (ExternalCall(SymbolId("python.os.system"), (frozenset({0}), frozenset()), frozenset({1}), span, None, arguments),), + ( + ExternalCall( + SymbolId("python.os.system"), + (frozenset({0}), frozenset()), + (("url", frozenset({1})), (None, frozenset({0, 1}))), + span, + None, + arguments, + ), + ), False, frozenset({SymbolId("python.builtins.input")}), ) diff --git a/tests/test_interprocedural.py b/tests/test_interprocedural.py index 87a9cbd..5565687 100644 --- a/tests/test_interprocedural.py +++ b/tests/test_interprocedural.py @@ -208,12 +208,15 @@ def test_return_values_record_the_external_results_they_depend_on() -> None: assert table.summary("const").return_externals == frozenset() -def test_keyword_arguments_are_conservatively_merged() -> None: - table = summaries("import subprocess\n\ndef run(cmd, flag):\n subprocess.run(cmd, check=flag)\n") +def test_keyword_arguments_keep_their_names() -> None: + table = summaries( + "import subprocess\n\ndef run(cmd, flag, options):\n subprocess.run(cmd, check=flag, **options)\n" + ) (call,) = table.summary("run").external_calls assert call.argument_dependencies == (frozenset({0}),) - assert call.keyword_dependencies == frozenset({1}) + # A sink may read one keyword only (``url=``); ``**options`` has no name. + assert call.keyword_dependencies == (("check", frozenset({1})), (None, frozenset({2}))) def test_recursive_summaries_reach_a_fixpoint() -> None: diff --git a/tests/test_ssrf_targets.py b/tests/test_ssrf_targets.py new file mode 100644 index 0000000..35cebc1 --- /dev/null +++ b/tests/test_ssrf_targets.py @@ -0,0 +1,124 @@ +"""SSRF sinks of the HTTP clients read the URL only: where the request goes. + +``requests.get(url, ...)`` and its siblings take the URL first; ``requests.request(method, +url)`` and ``httpx.stream(method, url)`` take it second; each accepts ``url=``. Attacker +data in the body, the JSON, the headers or the query parameters does not choose the host +the server connects to, so it is not a server-side request forgery. A summary keeps which +keyword a parameter reaches, so a project function forwarding ``url=`` still is one. +""" + +from __future__ import annotations + +import pytest + +from coretrace_python import engine +from coretrace_python.semantic.symbols import SymbolId +from coretrace_python.source import SourceManager +from coretrace_python.taint import Sink, TaintKind + +PREFIX = ( + "import httpx\nimport requests\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n" + "@app.route('/proxy')\ndef proxy():\n" +) + + +def ssrf(source: str) -> list[int]: + findings = engine.check(SourceManager().add_source("app.py", source), [engine.BUNDLED_PLUGINS]) + return sorted(f.span.start_line for f in findings if f.rule_id == "ssrf") + + +def sink(name: str) -> Sink: + manager = engine.build_manager(engine.build_hir(SourceManager().add_source("e.py", ""))) + table = engine.plugin_models(loaded.plugin for loaded in engine.load_plugins([engine.BUNDLED_PLUGINS], manager)) + found = table.sink(SymbolId(f"python.{name}")) + assert found is not None, name + return found + + +def test_a_kind_restricted_to_a_position_and_a_keyword_reaches_those_arguments_only() -> None: + fetch = Sink( + SymbolId("python.http.fetch"), + TaintKind.SSRF | TaintKind.ADVISORY, + positions=((TaintKind.SSRF, (0,)),), + keywords=((TaintKind.SSRF, ("url",)),), + ) + + assert fetch.kinds_at(0) == fetch.kinds_at(None, "url") == TaintKind.SSRF | TaintKind.ADVISORY + assert fetch.kinds_at(1) == fetch.kinds_at(None, "data") == TaintKind.ADVISORY + # A starred argument or ``**kwargs`` may hold anything: unknown is not the URL. + assert fetch.kinds_at(None) == TaintKind.ADVISORY + merged = fetch.merged(Sink(fetch.symbol, TaintKind.COMMAND)) + assert merged.kinds_at(None, "url") == TaintKind.SSRF | TaintKind.ADVISORY | TaintKind.COMMAND + + +@pytest.mark.parametrize( + "name, position, keyword", + [ + ("requests.get", 0, "url"), + ("requests.post", 0, "url"), + ("requests.request", 1, "url"), + ("requests.api.request", 1, "url"), + ("requests.Session.get", 0, "url"), + ("requests.Session.request", 1, "url"), + ("requests.Session.send", 0, "request"), + ("httpx.get", 0, "url"), + ("httpx.request", 1, "url"), + ("httpx.stream", 1, "url"), + ("httpx.Client.stream", 1, "url"), + ("httpx.AsyncClient.send", 0, "request"), + ], +) +def test_each_client_function_names_its_url_by_its_signature(name: str, position: int, keyword: str) -> None: + found = sink(name) + + assert found.positions == ((TaintKind.SSRF, (position,)),) + assert found.keywords == ((TaintKind.SSRF, (keyword,)),) + + +@pytest.mark.parametrize( + "call", + [ + "requests.get(request.args['u'], timeout=5)", + "requests.get(url=request.args['u'], timeout=5)", + "requests.post(request.args['u'], data='x', timeout=5)", + "requests.request('GET', request.args['u'], timeout=5)", + "requests.request('GET', url=request.args['u'], timeout=5)", + "requests.Session().get(request.args['u'], timeout=5)", + "requests.Session().request('GET', request.args['u'], timeout=5)", + "httpx.get(request.args['u'], timeout=5)", + "httpx.Client().post(url=request.args['u'], timeout=5)", + "httpx.stream('GET', request.args['u'], timeout=5)", + ], +) +def test_a_tainted_url_is_a_server_side_request_forgery(call: str) -> None: + assert ssrf(PREFIX + f" return {call}\n") == [9] + + +@pytest.mark.parametrize( + "call", + [ + "requests.post('https://api.example.com/x', data=request.args['d'], timeout=5)", + "requests.post('https://api.example.com/x', json={'k': request.args['d']}, timeout=5)", + "requests.get('https://api.example.com/x', params={'q': request.args['q']}, timeout=5)", + "requests.get('https://api.example.com/x', headers={'X-Id': request.args['h']}, timeout=5)", + "requests.request('PATCH', 'https://api.example.com/x', data={'t': request.args['t']}, timeout=5)", + "requests.request(request.args['m'], 'https://api.example.com/x', timeout=5)", + "httpx.Client().post('https://api.example.com/x', content=request.args['b'], timeout=5)", + ], +) +def test_attacker_data_that_does_not_choose_the_destination_is_no_ssrf(call: str) -> None: + assert ssrf(PREFIX + f" return {call}\n") == [] + + +def test_a_project_function_keeps_which_keyword_its_parameters_reach() -> None: + source = ( + "import requests\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n" + "def send(target, payload):\n" + " return requests.post(url=target, data=payload, timeout=5)\n\n" + "@app.route('/proxy')\n" + "def proxy():\n" + " send(request.args['u'], 'x')\n" + " send('https://api.example.com/x', request.args['d'])\n" + ) + + assert ssrf(source) == [11] From 6b65fa5ef5296c53930b9fc92a2aff234505b87c Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 24 Sep 2026 22:16:25 +0200 Subject: [PATCH 2/2] fix: make the HTTP client SSRF sinks read the request destination only --- CHANGELOG.md | 6 +++ docs/plugins.md | 4 +- docs/usage.md | 2 +- .../models/http_clients/http_client_models.py | 35 +++++++++------ src/coretrace_python/cache.py | 6 +-- .../interprocedural/summaries.py | 26 +++++++---- src/coretrace_python/taint/engine.py | 22 +++++++--- src/coretrace_python/taint/models.py | 44 ++++++++++++------- tests/regression/expected/httpie.json | 6 --- tests/regression/expected/pygoat.json | 6 --- 10 files changed, 97 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e1a63..8a0f1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Precision + +- The SSRF sinks of Requests and httpx read the destination only: the URL (the first argument, the second after the method for `request` and `stream`, or `url=`) or the prepared request `send` takes. Attacker data in the body, the JSON, the headers or the query parameters is no longer a server-side request forgery. `Sink` gains `keywords` next to `positions`, and function summaries keep the name of each keyword argument, so a project function forwarding `url=` still reaches the sink; the cache format is bumped (#128). + ## 0.6.0 (2026-09-24) ### Reports diff --git a/docs/plugins.md b/docs/plugins.md index e766074..e69f279 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -141,7 +141,7 @@ class BottleModels(ModelPlugin): | Model | Declares | |---|---| | `Source(symbol, label, kinds=ALL)` | A call or attribute whose value is attacker-controlled; `label` names it in messages (`http`, `stdin`, `argv`, `http-response`). `kinds` restricts what it can inject. | -| `Sink(symbol, kinds, positions=())` | A call dangerous for `kinds`; `positions` limits a kind to given argument indexes, as SQL sinks read the statement only. | +| `Sink(symbol, kinds, positions=(), keywords=())` | A call dangerous for `kinds`. `positions` limits a kind to the given argument indexes, and `keywords` to the given keyword names; a restricted kind reaches only the arguments listed there. SQL sinks read the statement only, and the SSRF sinks of HTTP clients read the URL, by position or as `url=`. A starred argument or `**kwargs` reaches no restricted kind. | | `Sanitizer(symbol, kinds)` | A call whose result is safe for `kinds`. | | `EntryPoint(symbol, label, kinds=ALL)` | A decorator or a class base whose functions receive their parameters as `label` input, such as `flask.Flask.route`. | | `TypedParameter(symbol, label, kinds=ALL)` | A class whose annotated parameters carry `label` input, such as Django's `HttpRequest`. | @@ -153,7 +153,7 @@ class BottleModels(ModelPlugin): | `AuthorizationGuard(symbol, label)` | A decorator or a condition restricting who reaches the code, such as `login_required`; a flow behind it is a hotspot. | Two plugins may describe the same symbol. An identical model is registered once; two -`Sink` models merge their kinds and positions, so a plugin can add `SQL` to a sink +`Sink` models merge their kinds, positions and keywords, so a plugin can add `SQL` to a sink another plugin declared for `COMMAND`; any other difference is a conflict that stops the analysis with a `ModelError` naming both plugins. diff --git a/docs/usage.md b/docs/usage.md index c749c6c..2a1e3a9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -108,7 +108,7 @@ a sanitizer for that kind of sink. | `command-injection` | Shell or process execution (`os.system`, `subprocess` with a string, …). | | `sql-injection` | A database statement (`cursor.execute`, SQLAlchemy `text`, Django `raw`, …). Parameters of a parameterised query are not statements. | | `path-traversal` | A file system path (`open`, `send_file`, `os.remove`, …). | -| `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`). | +| `ssrf` | The URL of an HTTP client request (Requests, httpx, `urllib`), by position or as `url=`; the body, headers and query parameters are not the destination. | | `xss` | An HTTP response body built without escaping. | | `insecure-deserialization` | `pickle`, `marshal`, `dill`, `jsonpickle`, `shelve`, and the YAML loaders (`yaml.load`, `load_all`, `full_load`, `unsafe_load` and their several-document versions); `yaml.load` and `load_all` given a safe loader (`SafeLoader`, `BaseLoader`) are not. | | `open-redirect` | An HTTP redirect target. | diff --git a/src/coretrace_python/bundled/models/http_clients/http_client_models.py b/src/coretrace_python/bundled/models/http_clients/http_client_models.py index f66f168..5c2a6e1 100644 --- a/src/coretrace_python/bundled/models/http_clients/http_client_models.py +++ b/src/coretrace_python/bundled/models/http_clients/http_client_models.py @@ -1,5 +1,8 @@ -"""Requests and httpx models: every request function is a SSRF sink, and what it returns -is an ``http-response`` source, so data fetched from a remote server is untrusted.""" +"""Requests and httpx models: every request function is a SSRF sink for the argument that +chooses the destination, and what it returns is an ``http-response`` source, so data +fetched from a remote server is untrusted. The destination is the URL — first, or second +after the method, or ``url=`` — or the prepared request a ``send`` takes; the body, the +headers and the query parameters do not choose the host.""" from __future__ import annotations @@ -20,15 +23,18 @@ "httpx.AsyncClient", ) -_REQUEST_FUNCTIONS = ( - *(f"{caller}.{method}" for caller in _CALLERS for method in _METHODS), - "requests.Session.send", - "httpx.stream", - "httpx.Client.stream", - "httpx.AsyncClient.stream", - "httpx.Client.send", - "httpx.AsyncClient.send", -) +# Where each function takes its destination, by position (the receiver of a method +# excluded) and by keyword, as its signature declares it. +_DESTINATIONS = { + **{f"{caller}.{method}": (0, "url") for caller in _CALLERS for method in _METHODS if method != "request"}, + **{f"{caller}.request": (1, "url") for caller in _CALLERS}, + "httpx.stream": (1, "url"), + "httpx.Client.stream": (1, "url"), + "httpx.AsyncClient.stream": (1, "url"), + "requests.Session.send": (0, "request"), + "httpx.Client.send": (0, "request"), + "httpx.AsyncClient.send": (0, "request"), +} def _sym(path: str) -> SymbolId: @@ -38,6 +44,9 @@ def _sym(path: str) -> SymbolId: class HttpClientModels(ModelPlugin): name: ClassVar[str] = "http-client-models" models: ClassVar[tuple[Model, ...]] = ( - *(Sink(_sym(function), TaintKind.SSRF) for function in _REQUEST_FUNCTIONS), - *(Source(_sym(function), "http-response") for function in _REQUEST_FUNCTIONS), + *( + Sink(_sym(function), TaintKind.SSRF, ((TaintKind.SSRF, (position,)),), ((TaintKind.SSRF, (keyword,)),)) + for function, (position, keyword) in _DESTINATIONS.items() + ), + *(Source(_sym(function), "http-response") for function in _DESTINATIONS), ) diff --git a/src/coretrace_python/cache.py b/src/coretrace_python/cache.py index 1baf0ec..c3d7313 100644 --- a/src/coretrace_python/cache.py +++ b/src/coretrace_python/cache.py @@ -41,7 +41,7 @@ from coretrace_python.semantic.symbols import SymbolId from coretrace_python.source import SourceId, SourceSpan -CACHE_FORMAT = 8 +CACHE_FORMAT = 9 @dataclass(frozen=True) @@ -233,7 +233,7 @@ def _encode_call(call: ExternalCall) -> dict[str, Any]: return { "symbol": str(call.symbol), "arguments": [sorted(deps) for deps in call.argument_dependencies], - "keywords": sorted(call.keyword_dependencies), + "keywords": [[name, sorted(deps)] for name, deps in call.keyword_dependencies], "location": _encode_span(call.location), "call_site": None if call.call_site is None else _encode_span(call.call_site), "given": _encode_arguments(call.arguments), @@ -245,7 +245,7 @@ def _decode_call(data: Mapping[str, Any]) -> ExternalCall: return ExternalCall( SymbolId(_string(data["symbol"])), tuple(_indices(deps) for deps in data["arguments"]), - _indices(data["keywords"]), + tuple((None if name is None else _string(name), _indices(deps)) for name, deps in data["keywords"]), _decode_span(data["location"]), None if site is None else _decode_span(site), _decode_arguments(data["given"]), diff --git a/src/coretrace_python/interprocedural/summaries.py b/src/coretrace_python/interprocedural/summaries.py index 030af3f..cc480ff 100644 --- a/src/coretrace_python/interprocedural/summaries.py +++ b/src/coretrace_python/interprocedural/summaries.py @@ -87,11 +87,13 @@ def __or__(self, other: Dep) -> Dep: @dataclass(frozen=True) class ExternalCall: - """An external symbol reached from this function; dependencies are parameter indices.""" + """An external symbol reached from this function; dependencies are parameter indices. + Keyword arguments keep their name, so a sink reading ``url=`` only can be decided; + ``*args`` and ``**kwargs`` have none.""" symbol: SymbolId argument_dependencies: tuple[Dependencies, ...] - keyword_dependencies: Dependencies + keyword_dependencies: tuple[tuple[str | None, Dependencies], ...] location: SourceSpan call_site: SourceSpan | None # What the arguments of the call at ``location`` denote, where it is made. @@ -313,9 +315,13 @@ def loaded(self, instruction: GetAttr | GetItem | GetIter, state: Mapping[Key, D def call(self, call: Call, state: dict[Key, Dep]) -> Dep: arguments = tuple(self.deep(a, state) for a in call.arguments) + named = ( + *((name, self.deep(value, state)) for name, value in call.keywords), + *((None, self.deep(value, state)) for value in call.starred), + ) keywords = EMPTY - for value in (*call.starred, *(v for _, v in call.keywords)): - keywords |= self.deep(value, state) + for _, deps in named: + keywords |= deps everything = keywords for deps in arguments: everything |= deps @@ -335,7 +341,7 @@ def call(self, call: Call, state: dict[Key, Dep]) -> Dep: self.record( target.symbol, tuple(a.parameters for a in arguments), - keywords.parameters, + tuple((name, deps.parameters) for name, deps in named), call.location, None, self.graph.arguments_at(self.name, call.location), @@ -418,7 +424,7 @@ def mapped(indices: Dependencies) -> Dep: self.record( reached.symbol, tuple(mapped(d).parameters for d in reached.argument_dependencies), - mapped(reached.keyword_dependencies).parameters, + tuple((name, mapped(d).parameters) for name, d in reached.keyword_dependencies), reached.location, call.location, reached.arguments, @@ -435,7 +441,7 @@ def record( self, symbol: SymbolId, arguments: tuple[Dependencies, ...], - keywords: Dependencies, + keywords: tuple[tuple[str | None, Dependencies], ...], location: SourceSpan, call_site: SourceSpan | None, given: Arguments, @@ -446,7 +452,11 @@ def record( arguments = tuple( a | b for a, b in zip(previous.argument_dependencies, arguments, strict=False) ) - keywords |= previous.keyword_dependencies + # The same call records the same keywords, in the same order. + keywords = tuple( + (name, deps | old) + for (name, deps), (_, old) in zip(keywords, previous.keyword_dependencies, strict=False) + ) self.external[key] = ExternalCall(symbol, arguments, keywords, location, call_site, given) diff --git a/src/coretrace_python/taint/engine.py b/src/coretrace_python/taint/engine.py index 5ae2238..85a00e9 100644 --- a/src/coretrace_python/taint/engine.py +++ b/src/coretrace_python/taint/engine.py @@ -10,7 +10,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping -from dataclasses import dataclass +from dataclasses import dataclass, replace from types import MappingProxyType from typing import ClassVar @@ -401,8 +401,12 @@ def external( if sink is not None: for position, argument in enumerate(call.arguments): self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, position, given) - for argument in (*call.starred, *(value for _, value in call.keywords)): + for argument in call.starred: self.report(flows, sink, self.rendered(argument, state), argument, call, None, None, None, given) + for name, argument in call.keywords: + self.report( + flows, sink, self.rendered(argument, state), argument, call, None, None, None, given, keyword=name + ) sanitizer = self.models.sanitizer(symbol) if sanitizer is not None: return everything.without(sanitizer.kinds) @@ -424,7 +428,7 @@ def guarded(self, sink: Sink | None, arguments: Arguments) -> Sink | None: if safe is None or given is None or not given[0] or given[1] not in safe.values: return sink kinds = sink.kinds & ~safe.kinds - return Sink(sink.symbol, kinds, sink.positions) if kinds else None + return replace(sink, kinds=kinds) if kinds else None def rendered(self, argument: Value, state: Mapping[Key, Taint]) -> Taint: """The taint an argument brings to a sink. A container literal is serialised by @@ -497,11 +501,16 @@ def mapped(deps: frozenset[int]) -> tuple[Taint, Value | None]: sink = self.guarded(self.models.sink(reached.symbol), reached.arguments) if sink is None: continue - positions: list[int | None] = [*range(len(reached.argument_dependencies)), None] - for position, deps in zip(positions, (*reached.argument_dependencies, reached.keyword_dependencies), strict=True): + for position, deps in enumerate(reached.argument_dependencies): taint, witness = mapped(deps) if witness is not None: self.report(flows, sink, taint, witness, call, through, reached.location, position, reached.arguments) + for name, deps in reached.keyword_dependencies: + taint, witness = mapped(deps) + if witness is not None: + self.report( + flows, sink, taint, witness, call, through, reached.location, None, reached.arguments, keyword=name + ) for mutation in summary.mutations: if mutation.parameter < len(values): stored = mapped(mutation.dependencies)[0] @@ -528,8 +537,9 @@ def report( sink_location: SourceSpan | None, position: int | None = None, arguments: Arguments | None = None, + keyword: str | None = None, ) -> None: - reaching = taint.kinds & sink.kinds_at(position) + reaching = taint.kinds & sink.kinds_at(position, keyword) if not reaching: return for source in sorted(taint.sources, key=lambda s: str(s.symbol)): diff --git a/src/coretrace_python/taint/models.py b/src/coretrace_python/taint/models.py index e40f109..249e114 100644 --- a/src/coretrace_python/taint/models.py +++ b/src/coretrace_python/taint/models.py @@ -55,28 +55,42 @@ class Source: @dataclass(frozen=True) class Sink: - """A callable whose arguments must not carry the given taint kinds. ``positions`` - restricts some kinds to argument positions: a SQL statement is the first argument - of ``execute``, its parameter tuple is not a statement.""" + """A callable whose arguments must not carry the given taint kinds. ``positions`` and + ``keywords`` restrict some kinds to the arguments they list: a SQL statement is the + first argument of ``execute``, its parameter tuple is not a statement; the target of + ``requests.get`` is its first argument or ``url=``, its body is no destination.""" symbol: SymbolId kinds: TaintKind positions: tuple[tuple[TaintKind, tuple[int, ...]], ...] = () - - def kinds_at(self, position: int | None) -> TaintKind: - """The kinds that must not reach the argument at ``position`` (``None`` for a - keyword or starred argument).""" - - kinds = self.kinds - for restricted, allowed in self.positions: - if position is None or position not in allowed: - kinds &= ~restricted - return kinds + keywords: tuple[tuple[TaintKind, tuple[str, ...]], ...] = () + + def kinds_at(self, position: int | None, keyword: str | None = None) -> TaintKind: + """The kinds that must not reach the argument at ``position`` or passed as + ``keyword``; both are ``None`` for a starred argument or ``**kwargs``, which + reach no restricted kind. A restricted kind reaches any argument one of its + restrictions lists.""" + + restricted = allowed = TaintKind(0) + for kinds, positions in self.positions: + restricted |= kinds + if position in positions: + allowed |= kinds + for kinds, names in self.keywords: + restricted |= kinds + if keyword in names: + allowed |= kinds + return self.kinds & ~(restricted & ~allowed) def merged(self, other: Sink) -> Sink: - """This sink with ``other``'s kinds and positions added.""" + """This sink with ``other``'s kinds, positions and keywords added.""" - return Sink(self.symbol, self.kinds | other.kinds, self.positions + other.positions) + return Sink( + self.symbol, + self.kinds | other.kinds, + self.positions + other.positions, + self.keywords + other.keywords, + ) @dataclass(frozen=True) diff --git a/tests/regression/expected/httpie.json b/tests/regression/expected/httpie.json index 58dac11..e8d6931 100644 --- a/tests/regression/expected/httpie.json +++ b/tests/regression/expected/httpie.json @@ -24,12 +24,6 @@ "rule": "ssrf", "function": "collect_messages" }, - { - "file": "httpie/client.py", - "line": 114, - "rule": "ssrf", - "function": "collect_messages" - }, { "file": "httpie/internal/update_warnings.py", "line": 44, diff --git a/tests/regression/expected/pygoat.json b/tests/regression/expected/pygoat.json index 60491d4..42cba2a 100644 --- a/tests/regression/expected/pygoat.json +++ b/tests/regression/expected/pygoat.json @@ -84,12 +84,6 @@ "rule": "missing-timeout", "function": "log_function_checker" }, - { - "file": "introduction/apis.py", - "line": 83, - "rule": "ssrf", - "function": "log_function_checker" - }, { "file": "introduction/apis.py", "line": 84,