{"record":{"id":"a0a86a4942051b15","repo":"dgtlmoon/changedetection.io","slug":"zip-slip-path-traversal-detected-in-backup-archive","errorCode":null,"errorMessage":"Zip Slip path traversal detected in backup archive: {member.filename!r}","messagePattern":"Zip Slip path traversal detected in backup archive: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"changedetectionio/blueprint/backups/restore.py","lineNumber":74,"sourceCode":"    skipped_watches = 0\n\n    current_tags = datastore.data['settings']['application'].get('tags', {})\n    current_watches = datastore.data['watching']\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        logger.debug(f\"Restore: extracting zip to {tmpdir}\")\n        with zipfile.ZipFile(zip_stream, 'r') as zf:\n            total_uncompressed = sum(m.file_size for m in zf.infolist())\n            if total_uncompressed > _MAX_DECOMPRESSED_BYTES:\n                raise ValueError(\n                    f\"Backup archive decompressed size ({total_uncompressed // (1024 * 1024)} MB) \"\n                    f\"exceeds the {_MAX_DECOMPRESSED_BYTES // (1024 * 1024)} MB limit\"\n                )\n            resolved_dest = os.path.realpath(tmpdir)\n            for member in zf.infolist():\n                member_dest = os.path.realpath(os.path.join(resolved_dest, member.filename))\n                if not member_dest.startswith(resolved_dest + os.sep) and member_dest != resolved_dest:\n                    raise ValueError(f\"Zip Slip path traversal detected in backup archive: {member.filename!r}\")\n                zf.extract(member, tmpdir)\n        logger.debug(\"Restore: zip extracted, scanning UUID directories\")\n\n        for entry in os.scandir(tmpdir):\n            if not entry.is_dir():\n                continue\n\n            uuid = entry.name\n            if not _UUID_RE.match(uuid):\n                logger.warning(f\"Restore: skipping non-UUID directory {uuid!r}\")\n                continue\n            tag_json_path = os.path.join(entry.path, 'tag.json')\n            watch_json_path = os.path.join(entry.path, 'watch.json')\n\n            # --- Tags (groups) ---\n            if include_groups and os.path.exists(tag_json_path):\n                if uuid in current_tags and not include_groups_replace:\n                    logger.debug(f\"Restore: skipping existing group {uuid} (replace not requested)\")","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/blueprint/backups/restore.py#L56-L92","documentation":"After stripping the 'json:' prefix, the expression is parsed with jsonpath_ng.ext.parse. If it raises JsonPathParserError or JsonPathLexerError the validator rejects the input with this message including the parser error text. It means the string is not a valid JSONPath expression per the jsonpath-ng extended syntax.","triggerScenarios":"Submitting 'json:' lines with malformed JSONPath such as 'json:$[', 'json:.foo..[' (bad recursion/filter syntax), unknown extensions, or unbalanced brackets/quotes that fail jsonpath_ng.ext.parse().","commonSituations":"Confusing JSONPath with jq syntax (e.g. 'json:.foo | .bar'); missing the leading $ ; bad filter expressions like $[?(@.a ==)]; typos from hand-writing paths instead of copying from a JSONPath tester.","solutions":["Validate in a REPL: from jsonpath_ng.ext import parse; parse('$.foo[*].bar')","Ensure the path starts with $ or a bracket expression and brackets/quotes are balanced","Use JSONPath testers and remember the extended syntax jsonpath-ng supports","Don't mix jq pipe syntax into json: lines — use jq: prefix instead"],"exampleFix":"# before\njson:.store.book[0].title\n# after\njson:$.store.book[0].title","handlingStrategy":"validation","validationCode":"from jsonpath_ng.ext import parse\nfrom jsonpath_ng.exceptions import JsonPathParserError, JsonPathLexerError\n\ndef jsonpath_ok(expr: str) -> bool:\n    try:\n        parse(expr)\n        return True\n    except (JsonPathParserError, JsonPathLexerError):\n        return False","typeGuard":"def is_valid_jsonpath(expr: str) -> bool:\n    try:\n        parse(expr)\n        return True\n    except (JsonPathParserError, JsonPathLexerError):\n        return False","tryCatchPattern":"try:\n    parse(expr)\nexcept (JsonPathParserError, JsonPathLexerError) as e:\n    # e.message / str(e) pinpoints the offending token\n    ...","preventionTips":["Always start JSONPath with $","Don't mix jq pipe syntax into json: expressions","Test paths with jsonpath_ng in a REPL or an online JSONPath tester"],"tags":["jsonpath","jsonpath-ng","form-validation"],"backgroundTag":"jsonpath-expression-invalid","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}