🛡️ Sentinel: [security improvement] Harden symlink handling and regex validators#96
Conversation
… validators Severity: Medium Vulnerability: Symlink content leakage and regex newline injection. Impact: Potential exfiltration of sensitive files via malicious symlinks and validation bypass via trailing newlines. Fix: - Explicitly set symlinks=True in shutil.copytree and follow_symlinks=False in shutil.copy2 across sync and publish flows. - Updated _is_valid_skill_name regex anchors from $ to \Z in discovery modules. Verification: Added regression tests in tests/test_security.py and verified all 550+ tests pass. Co-authored-by: renatocaliari <1719651+renatocaliari@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
There was a problem hiding this comment.
Code Review
This pull request hardens file operations and validation by ensuring that shutil.copytree preserves symlinks (symlinks=True), shutil.copy2 does not follow symlinks (follow_symlinks=False), and regex patterns use \Z instead of $ to prevent newline injection. Security tests were also added to verify these changes. The reviewer noted that Path.is_dir() returns True for symlinks pointing to directories, which can cause shutil.copytree to raise a FileExistsError on subsequent syncs when dirs_exist_ok=True is used. Additionally, broken symlinks are currently ignored. The reviewer suggested explicitly checking for symlinks first, unlinking existing destinations, and copying them using shutil.copy2 with follow_symlinks=False.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if item.is_dir(): | ||
| shutil.copytree(item, dest, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | ||
| shutil.copytree(item, dest, symlinks=True, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | ||
| else: | ||
| shutil.copy2(item, dest) | ||
| shutil.copy2(item, dest, follow_symlinks=False) |
There was a problem hiding this comment.
In Python, Path.is_dir() returns True for symbolic links pointing to directories. When item is a symlink pointing to a directory, calling shutil.copytree with symlinks=True and dirs_exist_ok=True will attempt to copy the symlink itself using os.symlink. However, shutil.copytree does not respect dirs_exist_ok=True when copying a root symlink, which will raise a FileExistsError if dest already exists (e.g., on subsequent syncs). Additionally, if item is a broken symlink, both is_dir() and is_file() return False, causing the symlink to be silently ignored.
To fix this, check item.is_symlink() first, remove the existing destination if it exists, and copy the symlink using shutil.copy2 with follow_symlinks=False.
if item.is_symlink():
if dest.exists() or dest.is_symlink():
dest.unlink()
shutil.copy2(item, dest, follow_symlinks=False)
elif item.is_dir():
shutil.copytree(item, dest, symlinks=True, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git'))
else:
shutil.copy2(item, dest, follow_symlinks=False)| if item.is_dir(): | ||
| shutil.copytree(item, item_dest, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | ||
| shutil.copytree(item, item_dest, symlinks=True, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | ||
| else: | ||
| shutil.copy2(item, item_dest) | ||
| shutil.copy2(item, item_dest, follow_symlinks=False) |
There was a problem hiding this comment.
In Python, Path.is_dir() returns True for symbolic links pointing to directories. When item is a symlink pointing to a directory, calling shutil.copytree with symlinks=True and dirs_exist_ok=True will attempt to copy the symlink itself using os.symlink. However, shutil.copytree does not respect dirs_exist_ok=True when copying a root symlink, which will raise a FileExistsError if item_dest already exists (e.g., on subsequent syncs). Additionally, if item is a broken symlink, both is_dir() and is_file() return False, causing the symlink to be silently ignored.
To fix this, check item.is_symlink() first, remove the existing destination if it exists, and copy the symlink using shutil.copy2 with follow_symlinks=False.
| if item.is_dir(): | |
| shutil.copytree(item, item_dest, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | |
| shutil.copytree(item, item_dest, symlinks=True, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | |
| else: | |
| shutil.copy2(item, item_dest) | |
| shutil.copy2(item, item_dest, follow_symlinks=False) | |
| if item.is_symlink(): | |
| if item_dest.exists() or item_dest.is_symlink(): | |
| item_dest.unlink() | |
| shutil.copy2(item, item_dest, follow_symlinks=False) | |
| elif item.is_dir(): | |
| shutil.copytree(item, item_dest, symlinks=True, dirs_exist_ok=True, ignore=shutil.ignore_patterns('.git')) | |
| else: | |
| shutil.copy2(item, item_dest, follow_symlinks=False) |
| if src_path.is_dir(): | ||
| shutil.copytree(src_path, dest, dirs_exist_ok=True, | ||
| shutil.copytree(src_path, dest, symlinks=True, dirs_exist_ok=True, | ||
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | ||
| else: | ||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copy2(src_path, dest) | ||
| shutil.copy2(src_path, dest, follow_symlinks=False) |
There was a problem hiding this comment.
In Python, Path.is_dir() returns True for symbolic links pointing to directories. When src_path is a symlink pointing to a directory, calling shutil.copytree with symlinks=True and dirs_exist_ok=True will attempt to copy the symlink itself using os.symlink. However, shutil.copytree does not respect dirs_exist_ok=True when copying a root symlink, which will raise a FileExistsError if dest already exists (e.g., on subsequent syncs). Additionally, if src_path is a broken symlink, both is_dir() and is_file() return False, causing the symlink to be silently ignored.
To fix this, check src_path.is_symlink() first, remove the existing destination if it exists, and copy the symlink using shutil.copy2 with follow_symlinks=False.
| if src_path.is_dir(): | |
| shutil.copytree(src_path, dest, dirs_exist_ok=True, | |
| shutil.copytree(src_path, dest, symlinks=True, dirs_exist_ok=True, | |
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | |
| else: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src_path, dest) | |
| shutil.copy2(src_path, dest, follow_symlinks=False) | |
| if src_path.is_symlink(): | |
| if dest.exists() or dest.is_symlink(): | |
| dest.unlink() | |
| shutil.copy2(src_path, dest, follow_symlinks=False) | |
| elif src_path.is_dir(): | |
| shutil.copytree(src_path, dest, symlinks=True, dirs_exist_ok=True, | |
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | |
| else: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(src_path, dest, follow_symlinks=False) |
| if skill.path.is_dir(): | ||
| shutil.copytree(skill.path, dest, dirs_exist_ok=True, | ||
| shutil.copytree(skill.path, dest, symlinks=True, dirs_exist_ok=True, | ||
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | ||
| else: | ||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copy2(skill.path, dest) | ||
| shutil.copy2(skill.path, dest, follow_symlinks=False) |
There was a problem hiding this comment.
In Python, Path.is_dir() returns True for symbolic links pointing to directories. When skill.path is a symlink pointing to a directory, calling shutil.copytree with symlinks=True and dirs_exist_ok=True will attempt to copy the symlink itself using os.symlink. However, shutil.copytree does not respect dirs_exist_ok=True when copying a root symlink, which will raise a FileExistsError if dest already exists (e.g., on subsequent syncs). Additionally, if skill.path is a broken symlink, both is_dir() and is_file() return False, causing the symlink to be silently ignored.
To fix this, check skill.path.is_symlink() first, remove the existing destination if it exists, and copy the symlink using shutil.copy2 with follow_symlinks=False.
| if skill.path.is_dir(): | |
| shutil.copytree(skill.path, dest, dirs_exist_ok=True, | |
| shutil.copytree(skill.path, dest, symlinks=True, dirs_exist_ok=True, | |
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | |
| else: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(skill.path, dest) | |
| shutil.copy2(skill.path, dest, follow_symlinks=False) | |
| if skill.path.is_symlink(): | |
| if dest.exists() or dest.is_symlink(): | |
| dest.unlink() | |
| shutil.copy2(skill.path, dest, follow_symlinks=False) | |
| elif skill.path.is_dir(): | |
| shutil.copytree(skill.path, dest, symlinks=True, dirs_exist_ok=True, | |
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | |
| else: | |
| dest.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(skill.path, dest, follow_symlinks=False) |
| if skill_item.is_dir(): | ||
| shutil.copytree(skill_item, dest_skill, dirs_exist_ok=True) | ||
| shutil.copytree(skill_item, dest_skill, symlinks=True, dirs_exist_ok=True) | ||
| else: | ||
| shutil.copy2(skill_item, dest_skill) | ||
| shutil.copy2(skill_item, dest_skill, follow_symlinks=False) |
There was a problem hiding this comment.
In Python, Path.is_dir() returns True for symbolic links pointing to directories. When skill_item is a symlink pointing to a directory, calling shutil.copytree with symlinks=True and dirs_exist_ok=True will attempt to copy the symlink itself using os.symlink. However, shutil.copytree does not respect dirs_exist_ok=True when copying a root symlink, which will raise a FileExistsError if dest_skill already exists (e.g., on subsequent syncs). Additionally, if skill_item is a broken symlink, both is_dir() and is_file() return False, causing the symlink to be silently ignored.
To fix this, check skill_item.is_symlink() first, remove the existing destination if it exists, and copy the symlink using shutil.copy2 with follow_symlinks=False.
| if skill_item.is_dir(): | |
| shutil.copytree(skill_item, dest_skill, dirs_exist_ok=True) | |
| shutil.copytree(skill_item, dest_skill, symlinks=True, dirs_exist_ok=True) | |
| else: | |
| shutil.copy2(skill_item, dest_skill) | |
| shutil.copy2(skill_item, dest_skill, follow_symlinks=False) | |
| if skill_item.is_symlink(): | |
| if dest_skill.exists() or dest_skill.is_symlink(): | |
| dest_skill.unlink() | |
| shutil.copy2(skill_item, dest_skill, follow_symlinks=False) | |
| elif skill_item.is_dir(): | |
| shutil.copytree(skill_item, dest_skill, symlinks=True, dirs_exist_ok=True) | |
| else: | |
| shutil.copy2(skill_item, dest_skill, follow_symlinks=False) |
| if item.is_dir(): | ||
| if not dry_run: | ||
| shutil.copytree(item, dest) | ||
| shutil.copytree(item, dest, symlinks=True) | ||
| synced += 1 | ||
| elif item.is_file(): | ||
| # Copy non-directory entries (e.g. RETIRED.md manifest). | ||
| # These are metadata files that restore the retirement | ||
| # state on a fresh machine. | ||
| if not dry_run: | ||
| shutil.copy2(item, dest) | ||
| shutil.copy2(item, dest, follow_symlinks=False) | ||
| synced += 1 |
There was a problem hiding this comment.
In Python, Path.is_dir() and Path.is_file() return False for broken symbolic links. If item is a broken symlink, it will be silently ignored and not copied. To ensure all symlinks (including broken ones) are preserved and copied correctly, check item.is_symlink() first.
| if item.is_dir(): | |
| if not dry_run: | |
| shutil.copytree(item, dest) | |
| shutil.copytree(item, dest, symlinks=True) | |
| synced += 1 | |
| elif item.is_file(): | |
| # Copy non-directory entries (e.g. RETIRED.md manifest). | |
| # These are metadata files that restore the retirement | |
| # state on a fresh machine. | |
| if not dry_run: | |
| shutil.copy2(item, dest) | |
| shutil.copy2(item, dest, follow_symlinks=False) | |
| synced += 1 | |
| if item.is_symlink(): | |
| if not dry_run: | |
| shutil.copy2(item, dest, follow_symlinks=False) | |
| synced += 1 | |
| elif item.is_dir(): | |
| if not dry_run: | |
| shutil.copytree(item, dest, symlinks=True) | |
| synced += 1 | |
| elif item.is_file(): | |
| # Copy non-directory entries (e.g. RETIRED.md manifest). | |
| # These are metadata files that restore the retirement | |
| # state on a fresh machine. | |
| if not dry_run: | |
| shutil.copy2(item, dest, follow_symlinks=False) | |
| synced += 1 |
This PR implements security hardening for symlink handling and input validation in
agent-sync.🛡️ Security Improvements
Symlink Content Leakage Protection:
shutil.copytreeandshutil.copy2calls insync.py,skills.py,git_publish.py, andsafe_fallback.py.symlinks=Trueandfollow_symlinks=Falserespectively.agent-syncfrom following symbolic links that point outside of the intended skill or agent directories, mitigating the risk of accidental or malicious data exfiltration. Also correctly preserves agent extension symlink structures.Regex Anchor Hardening:
_is_valid_skill_nameregular expressions inlocal_source.pyandexternal_source.py.$to\Z.my-skill\nare correctly rejected.✅ Verification
tests/test_security.pycovering both symlink preservation and newline injection.shutilcopy calls in the modified files have been hardened.PR created automatically by Jules for task 10497176329435694531 started by @renatocaliari