{"record":{"id":"f0edb29ceb1eef1f","repo":"github/spec-kit","slug":"errors-join","errorCode":null,"errorMessage":"errors.join('; ')","messagePattern":"errors\\.join\\('; '\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/specify_cli/events.py","lineNumber":1903,"sourceCode":"                    checks = \" || \".join(\n                        f\"input.tool === {json.dumps(t.lower())}\" for t in tools\n                    )\n                    body_lines.append(\n                        f\"    try {{ if ({checks}) {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} }} catch (e) {{ errors.push((e as Error).message); }}\"\n                    )\n                else:\n                    body_lines.append(\n                        f\"    try {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} catch (e) {{ errors.push((e as Error).message); }}\"\n                    )\n            elif is_injection:\n                body_lines.append(\n                    f\"    try {{ const ctx = runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); if (ctx) contexts.push(ctx); }} catch (e) {{ errors.push((e as Error).message); }}\"\n                )\n            else:\n                body_lines.append(\n                    f\"    try {{ runEvent({command_lit}, {ev_lit}, input, output, {timeout_sec}); }} catch (e) {{ errors.push((e as Error).message); }}\"\n                )\n        body_lines.append(\"    if (errors.length > 0) { throw new Error(errors.join('; ')); }\")\n\n        if native.startswith(\"tool.execute.\"):\n            ts_hook = native\n            event_entries.append(\n                f\"function _{ev}(input: any, output: any) {{\\n\"\n                + \"\\n\".join(body_lines) + \"\\n\"\n                \"  }\"\n            )\n            plugin_returns.append(\n                f\"    {json.dumps(ts_hook)}: async (input: any, output: any) => {{\\n\"\n                f\"      _{ev}(input, output);\\n\"\n                f\"    }},\"\n            )\n        elif native == \"experimental.chat.system.transform\":\n            body_lines.append('    return contexts.join(\"\\\\n\\\\n\");')\n            event_entries.append(\n                f\"function _{ev}(input: any, output: any): string {{\\n\"\n                + \"\\n\".join(body_lines) + \"\\n\"","sourceCodeStart":1885,"sourceCodeEnd":1921,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/events.py#L1885-L1921","documentation":"Second guard inside _shared_relative_path(): the path did relativize, but the result is absolute or contains a '..' component. This catches relative destinations like '../outside/file' that Path.relative_to can still produce when dest was built as project_path/'..'/'elsewhere', meaning the write would climb out of the project root.","triggerScenarios":"Calling shared-infra write helpers with dest such as project_path / '..' / 'sibling' / 'file', or a relative dest containing '..' segments that survives relative_to. E.g. _write_shared_bytes(project, project/'.specify'/'..'/'..'/'victim', b'x').","commonSituations":"User-configured subdir containing '..' (e.g. config value \"../shared\"); sanitizing paths by string concatenation instead of resolve(); templates or presets whose directory fields were edited to traverse upward.","solutions":["Normalize and validate the relative portion: reject any dest whose parts contain '..' before calling the API","Build destinations from trusted constants plus sanitized user input (allow only [A-Za-z0-9._-] segments)","Fix the configuration value that introduced the '..' segment","Run Path(os.path.normpath(dest)) checks in your own layer and fail early with a clearer message"],"exampleFix":"# before\ndest = project / cfg.shared_dir / 'file.yaml'  # cfg.shared_dir = '../outside'\n\n# after\nif '..' in Path(cfg.shared_dir).parts:\n    raise ValueError('shared_dir must stay inside the project')\ndest = project / cfg.shared_dir / 'file.yaml'","handlingStrategy":"validation","validationCode":"from pathlib import Path, PurePosixPath\n\ndef safe_relative(user_path: str) -> Path | None:\n    p = PurePosixPath(user_path)\n    if p.is_absolute() or '..' in p.parts:\n        return None\n    return Path(p)\n\nrel = safe_relative(cfg.shared_dir)\nif rel is None:\n    raise ValueError(f'unsafe shared dir: {cfg.shared_dir!r}')","typeGuard":null,"tryCatchPattern":"try:\n    rel = _shared_relative_path(project_path, dest)\nexcept ValueError as e:\n    if 'escapes project root' in str(e):\n        raise SystemExit(f'configuration points outside the project: {e}') from e\n    raise","preventionTips":["Sanitize directory config values with an allowlist pattern like ^[A-Za-z0-9._/-]+$ plus explicit '..' rejection","Never construct dest via string concatenation of user input; use Path.joinpath on validated parts","Add a unit test asserting '..' in user-supplied subdirs is rejected before the API is reached"],"tags":["filesystem","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}