Skip to content

🛡️ Sentinel: [HIGH] Harden publish flow against leakage and injection#117

Open
calionauta wants to merge 1 commit into
mainfrom
sentinel-harden-publish-flow-8613642642801538767
Open

🛡️ Sentinel: [HIGH] Harden publish flow against leakage and injection#117
calionauta wants to merge 1 commit into
mainfrom
sentinel-harden-publish-flow-8613642642801538767

Conversation

@calionauta

Copy link
Copy Markdown
Owner

This PR addresses several high-priority security issues in the publish flow that could lead to information disclosure or command/argument injection.

  1. Regex Newline Injection: Updated _is_valid_skill_name to use \Z instead of $, preventing skill names with trailing newlines from bypassing validation.
  2. Brittle Exclusion Logic: Replaced a manual, incomplete ignore function in git_publish.py with a robust fnmatch-based filter that correctly respects all DEFAULT_IGNORE_PATTERNS (e.g., sessions/, models.json).
  3. Symlink Content Leakage: Configured shutil operations to preserve symlinks as links rather than following them, preventing the accidental inclusion of sensitive system files in public skill repositories.

Verification:

  • New test suite tests/test_publish_security.py confirms all fixes.
  • All existing publishing tests pass.

PR created automatically by Jules for task 8613642642801538767 started by @renatocaliari

- Fix newline injection vulnerability in skill name validation by using \Z anchor.
- Refactor _ignore_func in git_publish.py to use robust fnmatch.filter matching.
- Prevent content leakage by disabling symlink following in shutil.copytree and shutil.copy2 during publish.
- Add comprehensive security regression tests in tests/test_publish_security.py.

Co-authored-by: renatocaliari <1719651+renatocaliari@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 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 @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request hardens the publishing flow against symlink traversal and regex injection by using absolute end-of-string anchors in regex validation, adopting fnmatch for pattern matching, and configuring shutil copy functions to not follow symlinks. The reviewer identified a critical edge case where is_dir() returns True for directory symlinks, which would still trigger shutil.copytree and leak content; they suggested checking is_symlink() first. Additionally, the reviewer recommended using shutil.ignore_patterns for cleaner code and corrected incomplete sentences in the sentinel documentation.

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.

Comment on lines 90 to +97
if src_path.is_dir():
shutil.copytree(src_path, dest, dirs_exist_ok=True,
# symlinks=True prevents content leakage by preserving links as links
shutil.copytree(src_path, dest, dirs_exist_ok=True, symlinks=True,
ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS))
else:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest)
# follow_symlinks=False ensures we don't accidentally copy target content
shutil.copy2(src_path, dest, follow_symlinks=False)

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.

security-high high

If src_path is a symbolic link pointing to a directory, src_path.is_dir() will return True. Calling shutil.copytree on it will follow the symlink and copy all of its target contents, which leads to the exact content leakage/information disclosure vulnerability this PR aims to prevent. To fix this, explicitly check if src_path is a symlink first and copy it as a symlink using shutil.copy2(..., follow_symlinks=False).

Suggested change
if src_path.is_dir():
shutil.copytree(src_path, dest, dirs_exist_ok=True,
# symlinks=True prevents content leakage by preserving links as links
shutil.copytree(src_path, dest, dirs_exist_ok=True, symlinks=True,
ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS))
else:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest)
# follow_symlinks=False ensures we don't accidentally copy target content
shutil.copy2(src_path, dest, follow_symlinks=False)
if src_path.is_symlink():
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_path, dest, follow_symlinks=False)
elif src_path.is_dir():
# symlinks=True prevents content leakage by preserving links as links
shutil.copytree(src_path, dest, dirs_exist_ok=True, symlinks=True,
ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS))
else:
dest.parent.mkdir(parents=True, exist_ok=True)
# follow_symlinks=False ensures we don't accidentally copy target content
shutil.copy2(src_path, dest, follow_symlinks=False)

Comment on lines 163 to +168
if skill.path.is_dir():
shutil.copytree(skill.path, dest, dirs_exist_ok=True,
shutil.copytree(skill.path, dest, dirs_exist_ok=True, symlinks=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)

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.

security-high high

If skill.path is a symbolic link pointing to a directory, skill.path.is_dir() will return True. Calling shutil.copytree on it will follow the symlink and copy all of its target contents, leading to content leakage. Explicitly check if skill.path is a symlink first and copy it as a symlink using shutil.copy2(..., follow_symlinks=False).

Suggested change
if skill.path.is_dir():
shutil.copytree(skill.path, dest, dirs_exist_ok=True,
shutil.copytree(skill.path, dest, dirs_exist_ok=True, symlinks=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():
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(skill.path, dest, follow_symlinks=False)
elif skill.path.is_dir():
shutil.copytree(skill.path, dest, dirs_exist_ok=True, symlinks=True,
ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS))
else:
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(skill.path, dest, follow_symlinks=False)

Comment thread .jules/sentinel.md
Comment on lines +7 to +8
**Vulnerability:** The public skill publishing flow was vulnerable to information disclosure via symbolic link traversal and brittle file exclusion logic. followed symlinks into sensitive areas, and the hand-rolled ignore function only handled a subset of intended patterns.
**Learning:** (before 3.12 or if not specified) and follow symlinks by default. Hand-rolling glob matching is error-prone; is the robust standard.

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.

medium

The sentences in this section are incomplete and contain missing words/subjects. Please complete them to ensure the documentation is clear and professional.

Suggested change
**Vulnerability:** The public skill publishing flow was vulnerable to information disclosure via symbolic link traversal and brittle file exclusion logic. followed symlinks into sensitive areas, and the hand-rolled ignore function only handled a subset of intended patterns.
**Learning:** (before 3.12 or if not specified) and follow symlinks by default. Hand-rolling glob matching is error-prone; is the robust standard.
**Vulnerability:** The public skill publishing flow was vulnerable to information disclosure via symbolic link traversal and brittle file exclusion logic. Copy operations followed symlinks into sensitive areas, and the hand-rolled ignore function only handled a subset of intended patterns.
**Learning:** `shutil.copytree` (before 3.12 or if not specified) and `shutil.copy2` follow symlinks by default. Hand-rolling glob matching is error-prone; `fnmatch.filter` is the robust standard.

Comment on lines +33 to 43
import fnmatch

def _ignore(path, names):
ignored = []
for name in names:
for pattern in patterns:
if pattern.startswith('*.'):
if name.endswith(pattern[1:]):
ignored.append(name)
break
elif pattern.startswith('.'):
if name == pattern or name.startswith(pattern.rstrip('/') + '/'):
ignored.append(name)
break
return ignored
for pattern in patterns:
# Use fnmatch.filter to robustly match patterns against names
matches = fnmatch.filter(names, pattern)
ignored.extend(matches)
return list(set(ignored)) # Deduplicate

return _ignore

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.

medium

Instead of manually re-implementing pattern filtering using fnmatch.filter, you can use Python's built-in shutil.ignore_patterns which is standard, highly optimized, and returns a set of ignored names directly.

Suggested change
import fnmatch
def _ignore(path, names):
ignored = []
for name in names:
for pattern in patterns:
if pattern.startswith('*.'):
if name.endswith(pattern[1:]):
ignored.append(name)
break
elif pattern.startswith('.'):
if name == pattern or name.startswith(pattern.rstrip('/') + '/'):
ignored.append(name)
break
return ignored
for pattern in patterns:
# Use fnmatch.filter to robustly match patterns against names
matches = fnmatch.filter(names, pattern)
ignored.extend(matches)
return list(set(ignored)) # Deduplicate
return _ignore
return shutil.ignore_patterns(*patterns)

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.

1 participant