# Security Findings Triage (2026-03-15)

- Baseline: `origin/main@226f10c2a62fc182b4e93458bddea2e60f9b0cb9`
- Input CSV was treated as triage input only, not as ground truth.
- Status meanings: `still present and exploitable`, `still present but low practical risk`, `obsolete/not reproducible on current HEAD`, `duplicate of another finding`.

## Summary

- still present and exploitable: 6
- obsolete/not reproducible on current HEAD: 6
- still present but low practical risk: 14
- duplicate of another finding: 7

## Remediation Buckets

- `codex/security-filesystem-trust-boundary`: findings 1, 3, 7, 10, 16, 20, 21, 27, 31, 32, 33 plus duplicates 5, 6, 8, 17, 22, 23.
- `codex/security-auth-integrity`: findings 12 and 19.
- `codex/security-shell-safety`: findings 4 and 24.
- `codex/security-robustness`: findings 9, 14, 15, 18, 29, 30.
- `codex/security-runtime-exploitable`: no standalone bucket remained after default-branch verification; the actionable issues all fit the filesystem/auth/shell/robustness buckets above.

## Detailed Findings

| # | Severity | Title | Current Paths | Status | Bucket | Why It Is / Is Not Valid On `origin/main` | Minimal Safe Fix | Target PR |
|---|---|---|---|---|---|---|---|---|
| 1 | high | Unsanitized frontmatter name enables path traversal in sync script | `tools/scripts/sync_microsoft_skills.py` | still present and exploitable | filesystem-trust-boundary | On origin/main, sync_microsoft_skills.py used the parsed frontmatter name directly under TARGET_DIR and cleanup_previous_sync reused flat_name from attribution without constraining it to skills/. | Sanitize flat names to a single safe path segment and refuse cleanup/copy targets that resolve outside the cloned repo or local skills/ root. | codex/security-filesystem-trust-boundary |
| 2 | medium | Stored XSS via rehype-raw rendering of skill markdown | `apps/web-app/src/pages/SkillDetail.tsx` | obsolete/not reproducible on current HEAD | n/a | On origin/main, SkillDetail renders markdown with react-markdown + remark-gfm + rehype-highlight only; rehype-raw is no longer imported or enabled. | n/a | n/a |
| 3 | medium | Symlink-following copy leaks host files in setup_web | `tools/scripts/setup_web.js` | still present and exploitable | filesystem-trust-boundary | On origin/main, setup_web.js used fs.statSync and recursive copy on skills/, so a symlink inside skills could resolve to an arbitrary host file or directory and be copied into public assets. | Resolve symlinks only when their real path stays inside skills/; otherwise skip them and keep copying regular entries. | codex/security-filesystem-trust-boundary |
| 4 | medium | Insecure install guidance allows remote script execution | `skills/apify-actorization/SKILL.md` | still present but low practical risk | shell-safety | On origin/main, the Apify actorization skill still recommended curl/irm pipe-to-shell installation and apify login -t, which is documentation-only but directly instructs unsafe execution and credential handling. | Replace pipe-to-shell commands with package-manager guidance and remove command-line token examples. | codex/security-shell-safety |
| 5 | medium | setup_web.js now follows symlinks, enabling file exfiltration | `tools/scripts/setup_web.js` | duplicate of another finding | filesystem-trust-boundary | Same origin/main behavior as finding 3: fs.statSync-based recursive copy in setup_web.js followed symlink targets during public asset setup. | Fix once in setup_web.js by constraining symlink resolution to the skills root. | codex/security-filesystem-trust-boundary |
| 6 | medium | Symlink traversal in web asset setup copies arbitrary files | `tools/scripts/setup_web.js` | duplicate of another finding | filesystem-trust-boundary | Same origin/main behavior as finding 3: the setup_web recursive copy followed symlink targets and copied their resolved content. | Fix once in setup_web.js by constraining symlink resolution to the skills root. | codex/security-filesystem-trust-boundary |
| 7 | medium | Symlink file copying in .github/skills sync leaks host files | `tools/scripts/sync_microsoft_skills.py` | still present and exploitable | filesystem-trust-boundary | On origin/main, find_skills_in_directory accepted symlinked skill dirs by item.resolve() and copy loops accepted regular files from resolved dirs without checking they remained under the clone root. | Reject symlink targets outside the clone root and copy only regular files whose resolved path stays under the clone root. | codex/security-filesystem-trust-boundary |
| 8 | medium | Symlinked file copy in Microsoft skill sync can leak host data | `tools/scripts/sync_microsoft_skills.py` | duplicate of another finding | filesystem-trust-boundary | Same origin/main behavior as finding 7: the Microsoft sync path trusted resolved symlink targets and copied files from them. | Fix once in sync_microsoft_skills.py by constraining resolved paths to the clone root. | codex/security-filesystem-trust-boundary |
| 9 | medium | Committed Python bytecode can hide malicious logic | `skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc | skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc` | still present but low practical risk | robustness | On origin/main, tracked __pycache__ artifacts were still present under skills/ui-ux-pro-max/scripts, which is review-hostile but not independently exploitable. | Remove tracked bytecode artifacts and rely on source-only review plus .gitignore. | codex/security-robustness |
| 10 | medium | Symlinked SKILL.md can leak host files via index script | `tools/scripts/generate_index.py` | still present but low practical risk | filesystem-trust-boundary | On origin/main, generate_index.py opened every SKILL.md it found via os.walk and did not skip symlinked SKILL.md files, so a malicious local symlink could exfiltrate another file into index metadata generation. | Skip symlinked SKILL.md files during indexing. | codex/security-filesystem-trust-boundary |
| 11 | low | Example loader trusts manifest paths, enabling file read | `docs/integrations/jetski-gemini-loader/loader.mjs` | obsolete/not reproducible on current HEAD | n/a | On origin/main, the loader example resolves the requested file and rejects any path whose path.relative escapes the configured skills root, so the reported direct file read no longer reproduces. | n/a | n/a |
| 12 | low | TLS certificate verification disabled in new scrapers | `skills/junta-leiloeiros/scripts/scraper/base_scraper.py | skills/junta-leiloeiros/scripts/web_scraper_fallback.py` | still present but low practical risk | auth-integrity | On origin/main, both the base scraper and the direct fallback client instantiated HTTP clients with verify=False / ignore_https_errors=True, which weakens transport integrity but is a local-run scraper risk rather than an application RCE. | Enable TLS verification by default and require an explicit environment opt-out for insecure targets. | codex/security-auth-integrity |
| 13 | low | Complete bundle omits valid skill categories | `tools/lib/skill-filter.js | tools/scripts/build-catalog.js | data/bundles.json` | obsolete/not reproducible on current HEAD | n/a | On origin/main, shipped bundle data is generated by tools/scripts/build-catalog.js into data/bundles.json; the reported omission in tools/lib/skill-filter.js does not drive current shipped catalog data. | n/a | n/a |
| 14 | low | Malformed frontmatter delimiter breaks YAML parsing for skills | `skills/alpha-vantage/SKILL.md | tools/lib/skill-utils.js` | still present but low practical risk | robustness | On origin/main, skills/alpha-vantage/SKILL.md still contained an extra delimiter token (--- Unknown), which caused parser warnings and broken metadata interpretation. | Repair the malformed frontmatter so the file is a valid YAML frontmatter document. | codex/security-robustness |
| 15 | low | ws_listener writes sensitive events to predictable /tmp files | `skills/videodb/scripts/ws_listener.py` | still present but low practical risk | robustness | On origin/main, ws_listener defaulted to /tmp for event, pid, and websocket-id files, which is a same-host local confidentiality risk rather than a remote exploit. | Default to a user-owned state directory instead of shared /tmp when no explicit output dir is provided. | codex/security-robustness |
| 16 | low | Symlink traversal lets /skills/ serve arbitrary local files | `apps/web-app/refresh-skills-plugin.js` | still present but low practical risk | filesystem-trust-boundary | On origin/main, refresh-skills-plugin.js used path.resolve(filePath).startsWith(...) and fs.statSync(filePath), so a symlink inside skills/ could still read a target outside the intended tree in local dev. | Resolve real paths and only serve files whose resolved path remains inside the skills root. | codex/security-filesystem-trust-boundary |
| 17 | low | Sync Skills endpoint follows symlinks from downloaded archive | `apps/web-app/refresh-skills-plugin.js` | duplicate of another finding | filesystem-trust-boundary | On origin/main, the stale Home.jsx path no longer exists, but the live issue is the same plugin root cause as finding 16: once symlinked content lands under skills/, the dev server trusts it by lexical path only. | Fix once in refresh-skills-plugin.js by resolving and constraining real paths. | codex/security-filesystem-trust-boundary |
| 18 | low | Validation crash if YAML frontmatter is not a mapping | `tools/scripts/validate_skills.py` | still present but low practical risk | robustness | On origin/main, validate_skills.parse_frontmatter returned yaml.safe_load output directly; scalar YAML values were not rejected before downstream key access. | Reject non-mapping frontmatter early and return a validation error instead of passing scalar values downstream. | codex/security-robustness |
| 19 | low | Anonymous Supabase writes allow skill star tampering | `apps/web-app/src/lib/supabase.ts | apps/web-app/src/hooks/useSkillStars.ts | apps/web-app/src/context/SkillContext.tsx` | still present and exploitable | auth-integrity | Inference from source: on origin/main, useSkillStars performed a direct upsert to skill_stars from frontend code using the public anon client. The repo contains no server-side gate or versioned policy proving that writes are constrained. | Disable shared frontend writes by default and keep only local starring unless an explicit deployment-time opt-in is provided. | codex/security-auth-integrity |
| 20 | low | Metadata fixer overwrites symlinked SKILL.md targets | `tools/scripts/fix_skills_metadata.py` | still present but low practical risk | filesystem-trust-boundary | On origin/main, fix_skills_metadata.py opened and rewrote every discovered SKILL.md without skipping symlinked files, so a crafted symlink could modify another file. | Skip symlinked SKILL.md files and only mutate real local skill files with mapping frontmatter. | codex/security-filesystem-trust-boundary |
| 21 | low | Installer now dereferences symlinks during copy | `tools/bin/install.js` | still present and exploitable | filesystem-trust-boundary | On origin/main, copyRecursiveSync used fs.statSync on cloned content, so a malicious symlink in the repo could copy arbitrary local files into the install target. | Use lstat, resolve symlinks only when they stay inside the cloned repo root, and skip/ignore out-of-root links. | codex/security-filesystem-trust-boundary |
| 22 | low | Installer merge path dereferences symlinks when copying | `tools/bin/install.js` | duplicate of another finding | filesystem-trust-boundary | Same origin/main behavior as finding 21: install.js dereferenced symlinks during install/merge copy. | Fix once in install.js by constraining or skipping symlink resolution. | codex/security-filesystem-trust-boundary |
| 23 | low | Cleanup sync deletes arbitrary paths via flat_name | `tools/scripts/sync_microsoft_skills.py` | duplicate of another finding | filesystem-trust-boundary | Same origin/main root cause as finding 1: cleanup_previous_sync used flat_name from attribution without constraining it to skills/. | Fix once in sync_microsoft_skills.py by sanitizing flat names before delete/copy operations. | codex/security-filesystem-trust-boundary |
| 24 | low | Audio transcription example allows Python code injection | `skills/audio-transcriber/examples/basic-transcription.sh` | still present but low practical risk | shell-safety | On origin/main, basic-transcription.sh used an unquoted heredoc and embedded $AUDIO_FILE/$MODEL/$TRANSCRIBER directly into Python source, so crafted input could break quoting and inject code in a local example script. | Use quoted heredocs and pass values through environment variables instead of interpolating them into Python source. | codex/security-shell-safety |
| 25 | low | Unbounded recursive skill traversal can crash catalog build | `tools/lib/skill-utils.js | tools/scripts/build-catalog.js` | obsolete/not reproducible on current HEAD | n/a | On origin/main, listSkillIdsRecursive walks Dirent directories from readdirSync({withFileTypes:true}); symlink entries are not treated as directories, so the reported unbounded symlink recursion does not reproduce. | n/a | n/a |
| 26 | low | Release scripts still use root skills_index.json path | `tools/scripts/update_readme.py | tools/scripts/generate_index.py | tools/scripts/release_cycle.sh` | obsolete/not reproducible on current HEAD | n/a | On origin/main, root skills_index.json is the canonical generated index and release_cycle.sh is only a wrapper around release_workflow.js, so the reported path mismatch no longer reproduces as a defect. | n/a | n/a |
| 27 | low | Symlink traversal in skill normalization allows file overwrite | `tools/lib/skill-utils.js | tools/scripts/normalize-frontmatter.js` | still present but low practical risk | filesystem-trust-boundary | On origin/main, listSkillIds used fs.statSync and fs.existsSync on child skill dirs, so normalize-frontmatter could treat symlinked skill folders as writable local skills. | Use lstat-based discovery and skip symlinked skill dirs / SKILL.md entries before normalization. | codex/security-filesystem-trust-boundary |
| 28 | low | last30days skill passes user input directly to Bash command | `skills/last30days/SKILL.md` | obsolete/not reproducible on current HEAD | n/a | On origin/main, the documented command passes "$ARGUMENTS" as a quoted argument to Python, so the reported direct Bash injection sink does not reproduce from the current text. | n/a | n/a |
| 29 | low | Unvalidated YAML frontmatter can crash index generation | `tools/scripts/generate_index.py` | duplicate of another finding | robustness | Same origin/main root cause as finding 18, but in generate_index.py instead of validate_skills.py: scalar YAML values were passed through without a mapping check. | Fix once by rejecting non-mapping frontmatter in both parser paths. | codex/security-robustness |
| 30 | low | Predictable /tmp counter file enables local file clobbering | `skills/cc-skill-strategic-compact/suggest-compact.sh` | still present but low practical risk | robustness | On origin/main, suggest-compact.sh stored state in /tmp/claude-tool-count-$$, which is predictable and shared-host local-only. | Move the counter file into a user-owned state directory. | codex/security-robustness |
| 31 | low | Symlink traversal risk in new sync script | `tools/scripts/sync_recommended_skills.sh` | still present but low practical risk | filesystem-trust-boundary | On origin/main, sync_recommended_skills.sh copied a fixed allowlist from the repo with cp -r, which is local-only but still trusts symlink handling in source content. | Use cp -RP so symlinks are preserved instead of dereferenced. | codex/security-filesystem-trust-boundary |
| 32 | low | skills_manager allows path traversal in enable/disable operations | `tools/scripts/skills_manager.py` | still present but low practical risk | filesystem-trust-boundary | On origin/main, enable_skill/disable_skill joined the user-supplied skill name directly under skills/.disabled and skills/, so ../ segments could escape the intended root. | Resolve the requested path and reject names that escape the intended skills directory. | codex/security-filesystem-trust-boundary |
| 33 | low | Zip Slip risk in Office unpack scripts | `skills/docx-official/ooxml/scripts/unpack.py | skills/pptx-official/ooxml/scripts/unpack.py` | still present and exploitable | filesystem-trust-boundary | On origin/main, both unpack.py scripts called ZipFile.extractall(output_path) directly, so a malicious Office archive could write outside the requested directory. | Validate each archive member path before extraction and reject path-traversal entries. | codex/security-filesystem-trust-boundary |
