{"record":{"id":"079c63abf366bf90","repo":"1Panel-dev/1Panel","slug":"unsafe-path","errorCode":null,"errorMessage":"unsafe-path","messagePattern":"unsafe-path","errorType":"console","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scripts/openresty-modules/diagnose-install.sh","lineNumber":217,"sourceCode":"import sys\n\nstate_path = pathlib.Path(sys.argv[1])\nmodules_root = pathlib.Path(sys.argv[2]).resolve()\nmodules = json.loads(state_path.read_text(encoding=\"utf-8\"))\nfailed = False\nprint(\"module\\tbuild_status\\ttarget_key\\tartifact\\texpected\\tactual\\tresult\")\nfor module in modules:\n    for build in module.get(\"builds\") or []:\n        target_key = (build.get(\"target\") or {}).get(\"key\", \"\")\n        for artifact in build.get(\"artifacts\") or []:\n            relative = artifact.get(\"path\", \"\")\n            expected = artifact.get(\"checksum\", \"\")\n            result = \"OK\"\n            actual = \"\"\n            try:\n                pure = pathlib.PurePosixPath(relative)\n                if not relative or pure.is_absolute() or \"..\" in pure.parts or \"\\\\\" in relative:\n                    raise ValueError(\"unsafe-path\")\n                candidate = modules_root / pathlib.Path(*pure.parts)\n                if candidate.is_symlink():\n                    raise ValueError(\"symlink-not-allowed\")\n                full_path = candidate.resolve(strict=True)\n                if modules_root not in full_path.parents:\n                    raise ValueError(\"outside-module-root\")\n                if not full_path.is_file():\n                    raise ValueError(\"not-regular-file\")\n                digest = hashlib.sha256()\n                with full_path.open(\"rb\") as handle:\n                    for chunk in iter(lambda: handle.read(1024 * 1024), b\"\"):\n                        digest.update(chunk)\n                actual = digest.hexdigest()\n                if actual.lower() != expected.lower():\n                    raise ValueError(\"checksum-mismatch\")\n            except Exception as error:\n                result = str(error)\n                failed = True","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/1Panel-dev/1Panel/blob/5ac7c808815b0691009cd390414f03471312262f/scripts/openresty-modules/diagnose-install.sh#L199-L235","documentation":"Raised by the embedded Python verifier in diagnose-install.sh when an artifact path recorded in the modules manifest is rejected before any filesystem access: it is empty, absolute, contains a '..' component, or contains a backslash. It is a strict path-sanitization gate (PurePosixPath parse) that guarantees the checksum step can only touch files under the modules root.","triggerScenarios":"Run scripts/openresty-modules/diagnose-install.sh (or its embedded heredoc Python) against a modules index JSON where an artifact's 'path' field is \"\", starts with '/', has a segment like '../..', or uses Windows separators ('\\\\') such as 'lua/\\\\resty\\\\foo.so'.","commonSituations":"Hand-edited manifest with absolute install paths; paths copied from a Windows machine; a build step recording its output directory as '..' relative to the workspace; empty path from a build that produced no artifact but still emitted a checksum entry.","solutions":["Fix the artifact 'path' in the modules index so it is a relative POSIX path under the modules root, e.g. 'resty/http.so' with no leading '/', no '..' segments, no backslashes","If the path came from a build log, regenerate the manifest with the build tool so paths are normalized via PurePosixPath(relative_to(modules_root))","Re-run diagnose-install.sh and confirm the row reports result=OK before continuing the install"],"exampleFix":"// manifest before\n{\"path\": \"/opt/1panel/openresty/modules/../resty/http.so\", \"checksum\": \"...\"}\n// manifest after\n{\"path\": \"resty/http.so\", \"checksum\": \"...\"}","handlingStrategy":"validation","validationCode":"# before calling the diagnostic, validate manifest paths\npython3 - <<'PY'\nimport json, pathlib, sys\nm = json.load(open(\"modules.json\"))\nbad = []\nfor mod in m:\n    for b in mod.get(\"builds\") or []:\n        for a in b.get(\"artifacts\") or []:\n            p = pathlib.PurePosixPath(a.get(\"path\", \"\"))\n            if not str(p) or p.is_absolute() or \"..\" in p.parts or \"\\\\\" in str(p):\n                bad.append(str(p))\nprint(\"bad paths:\", bad)\nsys.exit(1 if bad else 0)\nPY","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Generate artifact paths programmatically relative to the modules root; never hand-type them","Normalize with PurePosixPath and reject absolute/'..'/backslash in CI before shipping the manifest"],"tags":["path-traversal","validation","python","openresty"],"backgroundTag":null,"analyzedSha":"5ac7c808815b0691009cd390414f03471312262f","analyzedAt":"2026-08-15T14:02:06.953Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}