Skip to content

feat(.agents): Update project agent lockfiles#6662

Merged
ericapisani merged 1 commit into
masterfrom
ep/update-agent-skill-lockfiles
Jun 26, 2026
Merged

feat(.agents): Update project agent lockfiles#6662
ericapisani merged 1 commit into
masterfrom
ep/update-agent-skill-lockfiles

Conversation

@ericapisani

@ericapisani ericapisani commented Jun 25, 2026

Copy link
Copy Markdown
Member

Update the agent lockfiles to the latest and greatest

Expand the skill-scanner with a new check_structural_attacks() function that detects
attack vectors beyond text content: symlinks that resolve outside the skill directory,
YAML frontmatter hooks that execute shell commands automatically, !command pre-prompt
injection that runs at template expansion time, test files that auto-execute via pytest or
Jest discovery, npm lifecycle hooks in bundled package.json files, and PNG metadata text
that can inject instructions into multimodal LLMs.

Also adds Unicode Tag character detection (U+E0000 block) to the obfuscation checks —
these chars are invisible in all editors and terminals but are processed by LLM tokenizers,
making them a covert injection channel.

Document the new attack categories in dangerous-code-patterns.md and
prompt-injection-patterns.md. Fix script path references in SKILL.md to use relative paths
instead of the removed CLAUDE_SKILL_ROOT variable, and update the skill discovery search
order to prefer .agents/skills/ first.

Updates agents.lock to commit 89a1f01 and aligns resolved_path entries with the canonical
skills/ tree layout.

Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com>
@ericapisani ericapisani changed the title feat(.agents): Add structural attack detection to skill-scanner feat(.agents): Update project agent lockfiles Jun 25, 2026
@ericapisani ericapisani marked this pull request as ready for review June 25, 2026 16:41
@ericapisani ericapisani requested a review from a team as a code owner June 25, 2026 16:41

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 95f285d. Configure here.

f"the prompt. Command: {cmd}",
"evidence": line.strip()[:200],
"category": "Pre-prompt Injection",
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs trigger pre-prompt detector

Medium Severity

The ! pre-prompt regex scans all of `SKILL.md`, so documenting the attack syntax (e.g. the `!`command bullet on line 109) is reported as a high-severity “Pre-prompt Command” even though no shell runs at load time.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95f285d. Configure here.

"description": f"{desc}. Bundled test files execute as a side effect of running "
"the test suite — review file contents for hidden payloads.",
"category": "Test File RCE",
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate test-file scan findings

Low Severity

Test auto-discovery loops over every test_patterns entry without stopping after a match, so one Python file can match both test_*.py and *_test.py and emit two separate high-severity findings for the same path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95f285d. Configure here.

parts = chunk_data.split(b"\x00", 4)
if len(parts) >= 5:
keyword = parts[0].decode("ascii", errors="ignore")
value = parts[4][:200].decode("utf-8", errors="ignore")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect iTXt PNG metadata parse

Medium Severity

iTXt chunks are parsed by splitting on null bytes only, but the PNG spec places one-byte compression flag and method between fields without null separators, so parts[4] often is not the text payload and metadata findings can be wrong or missed.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 95f285d. Configure here.

Comment on lines +496 to +501
elif chunk_type == b"iTXt":
# iTXt: keyword\0comprFlag\0comprMethod\0langTag\0transKeyword\0text
parts = chunk_data.split(b"\x00", 4)
if len(parts) >= 5:
keyword = parts[0].decode("ascii", errors="ignore")
value = parts[4][:200].decode("utf-8", errors="ignore")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The code incorrectly parses PNG iTXt chunks by splitting on null bytes. It misinterprets the fixed-size comprFlag and comprMethod fields as null-terminated, leading to incorrect metadata extraction.
Severity: HIGH

Suggested Fix

The fix requires parsing the iTXt chunk according to its specification instead of relying on split. First, find the keyword by splitting on the first null byte. Then, read the next two bytes directly for comprFlag and comprMethod. The remaining data can then be split on the remaining null bytes to correctly extract the language tag, translated keyword, and the final text content.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: .agents/skills/skill-scanner/scripts/scan_skill.py#L496-L501

Potential issue: The PNG `iTXt` chunk parser at `scan_skill.py:496` incorrectly assumes
all fields are null-terminated. The PNG specification defines `comprFlag` and
`comprMethod` as single-byte binary fields, not null-terminated strings. The current
implementation uses `chunk_data.split(b"\x00", 4)`, which fails for uncompressed chunks
(the common case) where `comprFlag` is `0x00`. This creates an extra empty part in the
split result, causing the code to incorrectly identify the translated keyword as the
text content and miss the actual text. Consequently, the scanner will fail to detect
malicious text in the most common type of PNG text metadata.

Did we get this right? 👍 / 👎 to inform future reviews.

@@ -441,7 +599,10 @@ def scan_skill(skill_dir: Path) -> dict[str, Any]:

all_findings.extend(script_findings)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unhandled OSError in symlink section can abort the entire scan

If path.readlink() or path.resolve() raises an OSError (e.g. a permission-denied error on a symlink in a restricted directory), the exception propagates unhandled through this call and crashes scan_skill, returning no findings at all. Wrap the symlink loop body in a try/except OSError like the PNG and package.json sections already do.

Evidence
  • check_structural_attacks at line 380 loops over skill_dir.rglob("*") and calls path.readlink() and path.resolve() with no surrounding try/except.
  • Both Path.readlink() and Path.resolve() can raise OSError (e.g. EACCES, EIO).
  • The PNG section (lines 479–518) and package.json section each wrap their I/O in try/except (OSError, ...), but the symlink section has no equivalent guard.
  • An unhandled exception propagates through the call at line 601 (all_findings.extend(check_structural_attacks(...))) and exits scan_skill with an uncaught exception, discarding all findings gathered so far.

Identified by Warden code-review · 342-AW8

@@ -441,7 +599,10 @@ def scan_skill(skill_dir: Path) -> dict[str, Any]:

all_findings.extend(script_findings)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rglob("*") in check_structural_attacks() follows directory symlinks on Python <3.13, enabling filesystem-traversal DoS against the scanner

check_structural_attacks() enumerates the skill directory with skill_dir.rglob("*") (symlink scan, test-file scan) and skill_dir.rglob("*.png") (PNG scan) without disabling symlink following. On Python 3.9–3.12 (the script declares requires-python = ">=3.9"), pathlib.Path.rglob descends into directory symlinks, so a malicious skill containing a single directory symlink to / (or ~) makes the scanner enumerate and, for the PNG branch, read arbitrary files across the whole filesystem — the very class of attack the symlink check was added to catch. A circular symlink can additionally trigger runaway recursion/resource exhaustion. The is_symlink() guard at the top of the loop only fires after rglob has already begun descending into the symlinked subtree, so it does not prevent the traversal.

Evidence
  • check_structural_attacks() (def at line 380) calls skill_dir.rglob("*") at lines 385 and 437 and skill_dir.rglob("*.png") at line 475, none passing follow_symlinks=False.
  • The script header declares requires-python = ">=3.9"; pathlib.Path.rglob's follow_symlinks parameter was only added in Python 3.13, and on 3.9–3.12 ** recursion follows directory symlinks, so a directory symlink to / causes full-filesystem enumeration.
  • The if path.is_symlink() check at line 386 emits a finding for the symlink entry but runs during iteration, after rglob has already started descending into the linked tree, so it cannot stop the traversal.
  • The PNG branch (line 475) calls img_path.read_bytes() on every *.png reached via traversal, so a symlinked subtree lets it read PNGs outside the skill directory and surface their metadata in findings.
  • Fix: iterate with os.walk(skill_dir, followlinks=False) (or guard each rglob result by skipping when an ancestor is a symlink), instead of relying on rglob's symlink-following default.

Identified by Warden find-bugs · ZTP-EUB

@github-actions

Copy link
Copy Markdown
Contributor

Codecov Results 📊

89905 passed | ⏭️ 6240 skipped | Total: 96145 | Pass Rate: 93.51% | Execution Time: 325m 26s

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 100.00%. Project has 2402 uncovered lines.
❌ Project coverage is 89.9%. Comparing base (base) to head (head).

Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    89.93%    89.90%    -0.03%
==========================================
  Files          192       192         —
  Lines        23784     23784         —
  Branches      8210      8210         —
==========================================
+ Hits         21389     21382        -7
- Misses        2395      2402        +7
- Partials      1342      1342         —

Generated by Codecov Action

@ericapisani ericapisani merged commit 21486dc into master Jun 26, 2026
144 checks passed
@ericapisani ericapisani deleted the ep/update-agent-skill-lockfiles branch June 26, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants