async def test_false_positive(): # Test for false positive respx.route(host="raw.githubusercontent.com").pass_through() respx.get("http://perdu.com/").mock(return_value=httpx.Response( 200, text= "<html><head><title>Vous Etes Perdu ?</title></head><body><h1>Perdu sur l'Internet ?</h1> \ <h2>Pas de panique, on va vous aider</h2> \ <strong><pre> * <----- vous êtes ici</pre></strong></body></html>" )) persister = FakePersister() request = Request("http://perdu.com/") request.path_id = 1 crawler = AsyncCrawler("http://perdu.com/") options = {"timeout": 10, "level": 2} logger = Mock() module = mod_wapp(crawler, persister, logger, options, Event()) module.verbose = 2 await module.attack(request) assert not persister.additionals await crawler.close()
async def test_false_positive(): # Test for false positive respx.route(host="raw.githubusercontent.com").pass_through() respx.get("http://perdu.com/").mock(return_value=httpx.Response( 200, text= "<html><head><title>Vous Etes Perdu ?</title></head><body><h1>Perdu sur l'Internet ?</h1> \ <h2>Pas de panique, on va vous aider</h2> \ <strong><pre> * <----- vous êtes ici</pre></strong></body></html>" )) persister = AsyncMock() home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") base_dir = os.path.join(home_dir, ".wapiti") persister.CONFIG_DIR = os.path.join(base_dir, "config") request = Request("http://perdu.com/") request.path_id = 1 crawler = AsyncCrawler("http://perdu.com/") options = {"timeout": 10, "level": 2} module = ModuleWapp(crawler, persister, options, Event()) await module.attack(request) assert not persister.add_payload.call_count await crawler.close()
async def test_unregistered_cname(): # Test attacking all kind of parameter without crashing respx.route(host="perdu.com").mock(return_value=httpx.Response(200, text="Hello there")) async def resolve(qname, rdtype, raise_on_no_answer: bool = False): if qname.startswith("supercalifragilisticexpialidocious."): # No wildcard responses return [] if qname.startswith("admin.") and rdtype == "CNAME": return make_cname_answer("perdu.com", "unregistered.com") raise dns.resolver.NXDOMAIN("Yolo") with patch("wapitiCore.attack.mod_takeover.dns.asyncresolver.resolve") as mocked_resolve_: with patch("wapitiCore.attack.mod_takeover.dns.asyncresolver.Resolver.resolve") as mocked_resolve: mocked_resolve.side_effect = resolve mocked_resolve_.side_effect = resolve persister = AsyncMock() all_requests = [] request = Request("http://perdu.com/") request.path_id = 1 all_requests.append(request) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} module = ModuleTakeover(crawler, persister, options, Event()) for request in all_requests: await module.attack(request) assert persister.add_payload.call_args_list[0][1]["request"].hostname == "admin.perdu.com" assert "unregistered.com" in persister.add_payload.call_args_list[0][1]["info"] await crawler.close()
async def test_direct_upload(): respx.route(host="127.0.0.1").pass_through() persister = AsyncMock() request = Request("http://127.0.0.1:65084/xxe/outofband/upload.php", file_params=[[ "foo", ("bar.xml", "<xml>test</xml>", "application/xml") ], [ "calendar", ("calendar.xml", "<xml>test</xml>", "application/xml") ]]) request.path_id = 8 persister.get_path_by_id.return_value = request crawler = AsyncCrawler("http://127.0.0.1:65084/") options = { "timeout": 10, "level": 1, "external_endpoint": "http://wapiti3.ovh/", "internal_endpoint": "http://wapiti3.ovh/" } logger = Mock() module = mod_xxe(crawler, persister, logger, options, Event()) await module.attack(request) respx.get( "http://wapiti3.ovh/get_xxe.php?session_id=" + module._session_id ).mock(return_value=httpx.Response( 200, json={ "8": { "63616c656e646172": [{ "date": "2019-08-17T16:52:41+00:00", "url": "https://wapiti3.ovh/xxe_data/yolo/8/63616c656e646172/31337-0-192.168.2.1.txt", "ip": "192.168.2.1", "size": 999, "payload": "linux2" }] } })) assert not persister.add_payload.call_count await module.finish() assert persister.add_payload.call_count assert persister.add_payload.call_args_list[0][1][ "parameter"] == "calendar" await crawler.close()
async def test_false_positives(): respx.route(host="raw.githubusercontent.com").pass_through() # This one trigger a match based on content respx.get("http://perdu.com/opendir.php?/etc/passwd").mock( return_value=httpx.Response(200, text="root:0:0:") ) # A lot of cases will trigger because HTTP 200 is returned instead of 404 but false positive check should block them respx.route(host="perdu.com").mock( return_value=httpx.Response(200, text="Hello there") ) persister = AsyncMock() home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") base_dir = os.path.join(home_dir, ".wapiti") persister.CONFIG_DIR = os.path.join(base_dir, "config") temp_nikto_db = os.path.join(persister.CONFIG_DIR, "temp_nikto_db") with open(temp_nikto_db, "w") as fd: fd.writelines( [ "003270,539,d,/catinfo,GET,200,,,,,May be vulnerable to a buffer overflow. Request '/catinfo?',,\n", "003271,5407,a,/soap/servlet/soaprouter,GET,200,,,,,Oracle 9iAS SOAP components allow anonymous,,\n", "003272,543,7,/opendir.php?/etc/passwd,GET,root:,,,,,This PHP-Nuke CGI allows attackers to read,,\n" ] ) request = Request("http://perdu.com/") request.path_id = 1 request.status = 200 request.set_headers({"content-type": "text/html"}) persister.get_links.return_value = chain([request]) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2, "tasks": 20} module = ModuleNikto(crawler, persister, options, Event()) module.do_get = True module.NIKTO_DB = "temp_nikto_db" await module.attack(request) os.unlink(temp_nikto_db) assert persister.add_payload.call_count == 1 assert persister.add_payload.call_args_list[0][1]["module"] == "nikto" assert persister.add_payload.call_args_list[0][1]["category"] == _("Potentially dangerous file") assert persister.add_payload.call_args_list[0][1]["request"].url == ( "http://perdu.com/opendir.php?%2Fetc%2Fpasswd" ) assert ( "This PHP-Nuke CGI allows attackers to read" ) in persister.add_payload.call_args_list[0][1]["info"] await crawler.close()
async def test_out_of_band_body(): respx.route(host="127.0.0.1").pass_through() persister = AsyncMock() request = Request("http://127.0.0.1:65084/xxe/outofband/body.php", method="POST", post_params=[["placeholder", "yolo"]]) request.path_id = 42 persister.get_path_by_id.return_value = request persister.requests.append(request) crawler = AsyncCrawler("http://127.0.0.1:65084/") options = { "timeout": 10, "level": 1, "external_endpoint": "http://wapiti3.ovh/", "internal_endpoint": "http://wapiti3.ovh/" } logger = Mock() module = mod_xxe(crawler, persister, logger, options, Event()) respx.get( "http://wapiti3.ovh/get_xxe.php?session_id=" + module._session_id ).mock(return_value=httpx.Response( 200, json={ "42": { "72617720626f6479": [{ "date": "2019-08-17T16:52:41+00:00", "url": "https://wapiti3.ovh/xxe_data/yolo/3/72617720626f6479/31337-0-192.168.2.1.txt", "ip": "192.168.2.1", "size": 999, "payload": "linux2" }] } })) module.do_post = False await module.attack(request) assert not persister.add_payload.call_count await module.finish() assert persister.add_payload.call_count assert persister.add_payload.call_args_list[0][1][ "parameter"] == "raw body" assert "linux2" in persister.add_payload.call_args_list[0][1][ "request"].post_params await crawler.close()
async def make_request(method='get', api_url='', api_key='', data={}, retries=0): """ Make call to external urls using python request :param method: get|post (str) :param api_url: :param api_key: :param data: data to send in the request (dict) :param retries: :return: """ headers = {'content-type': 'application/json'} if api_key and isinstance(data, dict): data['api_key'] = api_key method = method.upper() respx.route(method=method, path=api_url).mock(return_value=httpx.Response( status_code=MOCK_URLS[api_url]['status_code'], json=MOCK_URLS[api_url]['data'])) event_hooks = {} if DEBUG_MODE: event_hooks = {'request': [log_request], 'response': [log_response]} async with httpx.AsyncClient(event_hooks=event_hooks) as client: try: async for attempt in AsyncRetrying( stop=stop_after_attempt(retries), wait=wait_fixed(2)): with attempt: if method == httpretty.GET: response = await client.get(api_url, params=data, headers=headers) elif method == httpretty.POST: response = await client.post( api_url, data=json.dumps(data, default=date_time_json_serialize), headers=headers) if response.status_code in STATUS_FORCE_LIST: response.raise_for_status() return { 'status_code': response.status_code, 'data': response.json() } except RetryError: return { 'status_code': INTERNAL_SERVER_STATUS_CODE, 'data': INTERNAL_SERVER_ERROR_MESSAGE }
async def test_hmac_signature() -> None: webhook = Webhook(url="http://localhost:8080/webhook", events="after:measure", secret="testing") config = WebhooksConfiguration(__root__=[webhook]) connector = WebhooksConnector(config=config) await connector.startup() info = {} def match_and_mock(request): if request.method != "POST": return None if "x-servo-signature" in request.headers: signature = request.headers["x-servo-signature"] body = request.read() info.update(dict(signature=signature, body=body)) return httpx.Response(204) webhook_request = respx.route().mock(side_effect=match_and_mock) await connector.dispatch_event("measure") assert webhook_request.called expected_signature = info["signature"] signature = str(hmac.new("testing".encode(), info["body"], hashlib.sha1).hexdigest()) assert signature == expected_signature
async def test_get_cashback_total_server_error(client: AsyncClient, access_token): response = {"statusCode": 500, "body": {"credit": 1993}} respx.route(host="localhost").pass_through() respx.get( "https://mdaqk8ek5j.execute-api.us-east-1.amazonaws.com/v1/cashback", params={ "cpf": "12312312323" }, ).mock(return_value=Response(500, json=response)) r = await client.get( f"{settings.API_V1_STR}/cashbacks/me", headers={"Authorization": f"Bearer {access_token}"}, ) assert r.status_code == 500 assert respx.calls.called
async def test_out_of_band_param(): respx.route(host="127.0.0.1").pass_through() persister = AsyncMock() request = Request( "http://127.0.0.1:65084/xxe/outofband/param.php?foo=bar&vuln=yolo") request.path_id = 7 persister.get_path_by_id.return_value = request crawler = AsyncCrawler("http://127.0.0.1:65084/") options = { "timeout": 10, "level": 1, "external_endpoint": "http://wapiti3.ovh/", "internal_endpoint": "http://wapiti3.ovh/" } logger = Mock() module = mod_xxe(crawler, persister, logger, options, Event()) respx.get( "http://wapiti3.ovh/get_xxe.php?session_id=" + module._session_id ).mock(return_value=httpx.Response( 200, json={ "7": { "76756c6e": [{ "date": "2019-08-17T16:52:41+00:00", "url": "https://wapiti3.ovh/xxe_data/yolo/7/76756c6e/31337-0-192.168.2.1.txt", "ip": "192.168.2.1", "size": 999, "payload": "linux2" }] } })) module.do_post = False await module.attack(request) assert not persister.add_payload.call_count await module.finish() assert persister.add_payload.call_count assert persister.add_payload.call_args_list[0][1]["parameter"] == "vuln" assert "linux2" in dict(persister.add_payload.call_args_list[0][1] ["request"].get_params)["vuln"] await crawler.close()
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.get("http://perdu.com/").mock( return_value=httpx.Response(200, text="Default page")) respx.get("http://perdu.com/admin/").mock( return_value=httpx.Response(401, text="Private section")) respx.route(method="ABC", host="perdu.com", path="/admin/").mock( return_value=httpx.Response(200, text="Hello there")) persister = AsyncMock() all_requests = [] request = Request("http://perdu.com/") request.path_id = 1 request.status = 200 request.set_headers({"content-type": "text/html"}) all_requests.append(request) request = Request("http://perdu.com/admin/") request.path_id = 2 request.status = 401 request.set_headers({"content-type": "text/html"}) all_requests.append(request) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} logger = Mock() module = mod_htaccess(crawler, persister, logger, options, Event()) module.verbose = 2 module.do_get = True for request in all_requests: if await module.must_attack(request): await module.attack(request) else: assert request.path_id == 1 assert persister.add_payload.call_count == 1 assert persister.add_payload.call_args_list[0][1]["module"] == "htaccess" assert persister.add_payload.call_args_list[0][1]["category"] == _( "Htaccess Bypass") assert persister.add_payload.call_args_list[0][1][ "request"].url == "http://perdu.com/admin/" await crawler.close()
async def test_out_of_band_query_string(): respx.route(host="127.0.0.1").pass_through() persister = AsyncMock() request = Request("http://127.0.0.1:65084/xxe/outofband/qs.php") request.path_id = 4 persister.get_path_by_id.return_value = request crawler = AsyncCrawler("http://127.0.0.1:65084/") options = { "timeout": 10, "level": 2, "external_endpoint": "http://wapiti3.ovh/", "internal_endpoint": "http://wapiti3.ovh/" } logger = Mock() module = mod_xxe(crawler, persister, logger, options, Event()) module.do_post = False await module.attack(request) respx.get( "http://wapiti3.ovh/get_xxe.php?session_id=" + module._session_id ).mock(return_value=httpx.Response( 200, json={ "4": { "51554552595f535452494e47": [{ "date": "2019-08-17T16:52:41+00:00", "url": "https://wapiti3.ovh/xxe_data/yolo/4/51554552595f535452494e47/31337-0-192.168.2.1.txt", "ip": "192.168.2.1", "size": 999, "payload": "linux2" }] } })) assert not persister.add_payload.call_count await module.finish() assert persister.add_payload.call_count assert persister.add_payload.call_args_list[0][1][ "parameter"] == "QUERY_STRING" await crawler.close()
def test_sync_app_route(using): from flask import Flask app = Flask("foobar") @app.route("/baz/") def baz(): return {"ham": "spam"} with respx.mock(using=using, base_url="https://foo.bar/") as respx_mock: app_route = respx_mock.route().mock(side_effect=WSGIHandler(app)) response = httpx.get("https://foo.bar/baz/") assert response.json() == {"ham": "spam"} assert app_route.called with respx.mock: respx.route(host="foo.bar").mock(side_effect=WSGIHandler(app)) response = httpx.get("https://foo.bar/baz/") assert response.json() == {"ham": "spam"}
async def test_decorating_test(client): assert respx.calls.call_count == 0 respx.calls.assert_not_called() request = respx.route(url="https://foo.bar/", name="home").respond(202) response = await client.get("https://foo.bar/") assert request.called is True assert response.status_code == 202 assert respx.calls.call_count == 1 assert respx.routes["home"].call_count == 1 respx.calls.assert_called_once() respx.routes["home"].calls.assert_called_once()
async def test_async_app_route(client, using): from starlette.applications import Starlette from starlette.responses import JSONResponse from starlette.routing import Route async def baz(request): return JSONResponse({"ham": "spam"}) app = Starlette(routes=[Route("/baz/", baz)]) async with respx.mock(using=using, base_url="https://foo.bar/") as respx_mock: app_route = respx_mock.route().mock(side_effect=ASGIHandler(app)) response = await client.get("https://foo.bar/baz/") assert response.json() == {"ham": "spam"} assert app_route.called async with respx.mock: respx.route(host="foo.bar").mock(side_effect=ASGIHandler(app)) response = await client.get("https://foo.bar/baz/") assert response.json() == {"ham": "spam"}
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.route(host="raw.githubusercontent.com").pass_through() respx.get("http://perdu.com/cgi-bin/a1disp3.cgi?../../../../../../../../../../etc/passwd").mock( return_value=httpx.Response(200, text="root:0:0:") ) respx.route(host="perdu.com").mock( return_value=httpx.Response(404, text="Not found") ) persister = AsyncMock() home_dir = os.getenv("HOME") or os.getenv("USERPROFILE") base_dir = os.path.join(home_dir, ".wapiti") persister.CONFIG_DIR = os.path.join(base_dir, "config") request = Request("http://perdu.com/") request.path_id = 1 request.status = 200 request.set_headers({"content-type": "text/html"}) persister.get_links.return_value = chain([request]) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} module = mod_nikto(crawler, persister, options, Event()) module.verbose = 2 module.do_get = True await module.attack(request) assert persister.add_payload.call_count == 1 assert persister.add_payload.call_args_list[0][1]["module"] == "nikto" assert persister.add_payload.call_args_list[0][1]["category"] == _("Potentially dangerous file") assert persister.add_payload.call_args_list[0][1]["request"].url == ( "http://perdu.com/cgi-bin/a1disp3.cgi?..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd" ) assert ( "This CGI allows attackers read arbitrary files on the host" ) in persister.add_payload.call_args_list[0][1]["info"] await crawler.close()
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.get("http://perdu.com/").mock( return_value=httpx.Response(200, text="Default page")) respx.get("http://perdu.com/admin/").mock( return_value=httpx.Response(401, text="Private section")) respx.route(method="ABC", host="perdu.com", path="/admin/").mock( return_value=httpx.Response(200, text="Hello there")) persister = FakePersister() request = Request("http://perdu.com/") request.path_id = 1 request.status = 200 request.set_headers({"content-type": "text/html"}) persister.requests.append(request) request = Request("http://perdu.com/admin/") request.path_id = 2 request.status = 401 request.set_headers({"content-type": "text/html"}) persister.requests.append(request) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} logger = Mock() module = mod_htaccess(crawler, persister, logger, options, Event()) module.verbose = 2 module.do_get = True for request in persister.requests: if module.must_attack(request): await module.attack(request) else: assert request.path_id == 1 assert persister.vulnerabilities assert persister.vulnerabilities[0].url == "http://perdu.com/admin/" await crawler.close()
async def test_no_ports(ports): """Test that no ports also work.""" route = respx.route( url__startswith=f"http://{HOST}/axis-cgi/param.cgi").respond( text="", headers={"Content-Type": "text/plain"}, ) await ports.update() assert route.call_count == 3 assert len(ports.values()) == 0
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.route(url__regex=r"http://perdu.com/.*").mock( return_value=httpx.Response(200, text="Hello there")) persister = AsyncMock() all_requests = [] request = Request("http://perdu.com/") request.path_id = 1 all_requests.append(request) request = Request("http://perdu.com/?foo=bar") request.path_id = 2 all_requests.append(request) request = Request("http://perdu.com/?foo=bar", post_params=[["a", "b"]], file_params=[[ "file", ("calendar.xml", "<xml>Hello there</xml", "application/xml") ]]) request.path_id = 3 all_requests.append(request) crawler = AsyncCrawler("http://perdu.com/", timeout=1) options = {"timeout": 10, "level": 2} logger = Mock() module = mod_redirect(crawler, persister, logger, options, Event()) module.verbose = 2 module.do_post = True for request in all_requests: await module.attack(request) assert True await crawler.close()
async def test_whole_stuff(): # Test attacking all kind of parameter without crashing respx.route(host="perdu.com").mock( return_value=httpx.Response(200, text="Hello there")) persister = AsyncMock() all_requests = [] request = Request("http://perdu.com/") request.path_id = 1 all_requests.append(request) request = Request("http://perdu.com/?foo=bar") request.path_id = 2 all_requests.append(request) request = Request("http://perdu.com/?foo=bar", post_params=[["a", "b"]], file_params=[[ "file", ("calendar.xml", b"<xml>Hello there</xml", "application/xml") ]]) request.path_id = 3 all_requests.append(request) def get_path_by_id(request_id): for req in all_requests: if req.path_id == int(request_id): return req return None persister.get_path_by_id.side_effect = get_path_by_id crawler = AsyncCrawler(Request("http://perdu.com/"), timeout=1) options = {"timeout": 10, "level": 2} module = ModuleSsrf(crawler, persister, options, Event()) module.do_post = True respx.get( "https://wapiti3.ovh/get_ssrf.php?session_id=" + module._session_id ).mock(return_value=httpx.Response( 200, json={ "3": { "66696c65": [{ "date": "2019-08-17T16:52:41+00:00", "url": "https://wapiti3.ovh/ssrf_data/yolo/3/66696c65/31337-0-192.168.2.1.txt", "ip": "192.168.2.1", "method": "GET" }] } })) for request in all_requests: await module.attack(request) assert not persister.add_payload.call_count # We must trigger finish() normally called by wapiti.py await module.finish() assert persister.add_payload.call_count assert persister.add_payload.call_args_list[0][1]["module"] == "ssrf" assert persister.add_payload.call_args_list[0][1]["category"] == _( "Server Side Request Forgery") assert persister.add_payload.call_args_list[0][1]["parameter"] == "file" assert persister.add_payload.call_args_list[0][1][ "request"].file_params == [[ 'file', ('http://external.url/page', b'<xml>Hello there</xml', 'application/xml') ]] await crawler.close()
def respx_add_empty_msg_pack(self, url, method='GET'): respx.route(method=method, url=url).return_value = Response( status_code=200, headers={'content-type': 'application/x-msgpack'}, content=msgpack.packb({}))
async def test_ports(ports): """Test that different types of ports work.""" update_ports_route = respx.route( url__startswith=f"http://{HOST}/axis-cgi/param.cgi").respond( text="""root.Input.NbrOfInputs=3 root.IOPort.I0.Direction=input root.IOPort.I0.Usage=Button root.IOPort.I1.Configurable=no root.IOPort.I1.Direction=input root.IOPort.I1.Input.Name=PIR sensor root.IOPort.I1.Input.Trig=closed root.IOPort.I2.Direction=input root.IOPort.I2.Usage= root.IOPort.I2.Output.Active=closed root.IOPort.I2.Output.Button=none root.IOPort.I2.Output.DelayTime=0 root.IOPort.I2.Output.Mode=bistable root.IOPort.I2.Output.Name=Output 2 root.IOPort.I2.Output.PulseTime=0 root.IOPort.I3.Direction=output root.IOPort.I3.Usage=Tampering root.IOPort.I3.Output.Active=open root.IOPort.I3.Output.Button=none root.IOPort.I3.Output.DelayTime=0 root.IOPort.I3.Output.Mode=bistable root.IOPort.I3.Output.Name=Tampering root.IOPort.I3.Output.PulseTime=0 root.Output.NbrOfOutputs=1 """, headers={"Content-Type": "text/plain"}, ) action_low_route = respx.get( f"http://{HOST}:80/axis-cgi/io/port.cgi?action=4%3A%2F") action_high_route = respx.get( f"http://{HOST}:80/axis-cgi/io/port.cgi?action=4%3A%5C") await ports.update() assert update_ports_route.call_count == 3 assert ports["0"].id == "0" assert ports["0"].configurable is False assert ports["0"].direction == "input" assert ports["0"].name == "" await ports["0"].action(action=ACTION_LOW) assert not action_low_route.called assert ports["1"].id == "1" assert ports["1"].configurable is False assert ports["1"].direction == "input" assert ports["1"].name == "PIR sensor" assert ports["1"].input_trig == "closed" assert ports["2"].id == "2" assert ports["2"].configurable is False assert ports["2"].direction == "input" assert ports["2"].name == "" assert ports["2"].output_active == "closed" assert ports["3"].id == "3" assert ports["3"].configurable is False assert ports["3"].direction == "output" assert ports["3"].name == "Tampering" assert ports["3"].output_active == "open" await ports["3"].close() assert action_low_route.called assert action_low_route.calls.last.request.method == "GET" assert action_low_route.calls.last.request.url.path == "/axis-cgi/io/port.cgi" assert action_low_route.calls.last.request.url.query.decode( ) == "action=4%3A%2F" await ports["3"].open() assert action_high_route.called assert action_high_route.calls.last.request.method == "GET" assert action_high_route.calls.last.request.url.path == "/axis-cgi/io/port.cgi" assert action_high_route.calls.last.request.url.query.decode( ) == "action=4%3A%5C"