diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 57e6d80e2..43aa273af 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -115,7 +115,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -464,7 +463,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -488,7 +486,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -498,7 +495,6 @@ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", - "peer": true, "dependencies": { "@emotion/memoize": "^0.9.0" } @@ -1524,7 +1520,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1752,7 +1747,6 @@ "integrity": "sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.21.0" } @@ -1783,7 +1777,6 @@ "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1795,7 +1788,6 @@ "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" } @@ -2132,7 +2124,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -3272,7 +3263,6 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -3289,7 +3279,6 @@ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", @@ -3540,9 +3529,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -3763,7 +3752,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", @@ -3973,7 +3961,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3986,7 +3973,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -4016,7 +4002,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4147,8 +4132,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4649,7 +4633,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -4843,7 +4826,6 @@ "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -4937,7 +4919,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/package.json b/frontend/package.json index 8c8d0ea0b..211c72975 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,6 +55,7 @@ "react-router": "^6.30.4", "dompurify": "^3.4.10", "@babel/core": "^7.29.7", - "undici": "^8.10.0" + "undici": "^8.10.0", + "nanoid": "^3.3.18" } } diff --git a/testing/backend/unit/test_parser_sandbox.py b/testing/backend/unit/test_parser_sandbox.py index 6667b6b9f..33759c7ab 100644 --- a/testing/backend/unit/test_parser_sandbox.py +++ b/testing/backend/unit/test_parser_sandbox.py @@ -40,7 +40,7 @@ def _write_parser(tmp_path: Path, body: str) -> Path: """Write a parser.py with the given body and return its path.""" p = tmp_path / "parser.py" - p.write_text(textwrap.dedent(body)) + p.write_text(textwrap.dedent(body), encoding="utf-8") return p @@ -403,3 +403,92 @@ def test_stderr_excerpt_truncated_to_2000_chars(self): def test_str_contains_plugin_id(self): err = ParserSandboxError("my_plugin", "bad thing") assert "my_plugin" in str(err) + + +# --------------------------------------------------------------------------- +# Multibyte UTF-8 Output & Boundary Coverage +# --------------------------------------------------------------------------- + + +class TestMultibyteUtf8OutputBoundaries: + """Coverage for multibyte UTF-8 output byte boundaries and safe failure handling.""" + + def test_multibyte_utf8_output_within_boundary(self, tmp_path): + """Parser output with multibyte UTF-8 characters within limit decodes cleanly.""" + p = _write_parser( + tmp_path, + """\ + def parse(output): + return {"summary": "Scan result: 🚀 日本語 € ñ", "count": 42} + """, + ) + result = run_parser_in_sandbox(p, "utf8_valid", "input") + assert result["summary"] == "Scan result: 🚀 日本語 € ñ" + assert result["count"] == 42 + + def test_multibyte_utf8_output_straddling_byte_limit(self, tmp_path): + """Streaming multibyte UTF-8 output crossing size limit on a byte boundary raises safely.""" + # 4-byte UTF-8 emoji repeated to cross max_output_bytes boundary mid-character + p = _write_parser( + tmp_path, + """\ + import sys + def parse(output): + # Write 4-byte UTF-8 character (🔥 = b'\\xf0\\x9f\\x94\\xa5') repeatedly + emoji = "🔥".encode("utf-8") + for _ in range(500): + sys.stdout.buffer.write(emoji) + sys.stdout.buffer.flush() + return {} + """, + ) + cap = 200 # Cap falls mid-stream across multibyte boundary + with pytest.raises(ParserSandboxError) as exc_info: + run_parser_in_sandbox(p, "utf8_overflow", "input", max_output_bytes=cap) + + assert "limit" in exc_info.value.reason or "exceeded" in exc_info.value.reason + assert exc_info.value.plugin_id == "utf8_overflow" + # Exception string formatting must be safe and not raise UnicodeDecodeError + assert "utf8_overflow" in str(exc_info.value) + + def test_multibyte_utf8_stderr_truncation_boundary_safety(self, tmp_path): + """Oversized multibyte UTF-8 stderr exceeding 64KB boundary is safely decoded and truncated.""" + p = _write_parser( + tmp_path, + """\ + import sys + def parse(output): + # Write 20,000 4-byte emojis (= 80,000 bytes > 65,536 bytes _MAX_STDERR_BYTES cap) + msg = ("🚀" * 20000).encode("utf-8") + sys.stderr.buffer.write(msg) + sys.stderr.buffer.flush() + raise RuntimeError("exploded after stderr flood") + """, + ) + with pytest.raises(ParserSandboxError) as exc_info: + run_parser_in_sandbox(p, "utf8_stderr_flood", "input") + + assert exc_info.value.plugin_id == "utf8_stderr_flood" + # Stderr excerpt must be decoded safely without UnicodeDecodeError + excerpt = exc_info.value.stderr_excerpt + assert len(excerpt) <= 2000 + assert isinstance(excerpt, str) + + def test_multibyte_utf8_non_json_stdout_truncation(self, tmp_path): + """Non-JSON stdout with invalid/partial multibyte UTF-8 sequence is safely handled.""" + p = _write_parser( + tmp_path, + """\ + import sys + def parse(output): + # Write invalid UTF-8 byte sequence to stdout + sys.stdout.buffer.write(b"not json: \\xf0\\x9f\\x99\\x80 partial: \\xf0\\x9f") + sys.stdout.buffer.flush() + return {} + """, + ) + with pytest.raises(ParserSandboxError) as exc_info: + run_parser_in_sandbox(p, "utf8_invalid_json", "input") + + assert "non-JSON output" in exc_info.value.reason + assert exc_info.value.plugin_id == "utf8_invalid_json"