-
Notifications
You must be signed in to change notification settings - Fork 0
π‘οΈ Sentinel: [HIGH] Harden publish flow against data leakage and symlink vulnerabilities #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,6 @@ | |
| """ | ||
|
|
||
| import shutil | ||
| import re | ||
| import subprocess | ||
| import tempfile | ||
| from pathlib import Path | ||
|
|
@@ -25,26 +24,10 @@ | |
| 'models.json', 'models.yaml', 'config_local.json', | ||
| # Environment files | ||
| '.env', '.env.*', '*.pem', '*.key', | ||
| # Security/Secret patterns | ||
| 'secrets', 'credentials', 'tokens', 'api_keys', 'mcp-secrets', | ||
| ] | ||
|
Comment on lines
24
to
29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To securely handle symlinks during directory copying without breaking functionality or leaking local paths, we can define a custom ignore helper. This helper will use 'models.json', 'models.yaml', 'config_local.json',
# Environment files
'.env', '.env.*', '*.pem', '*.key',
# Security/Secret patterns
'secrets', 'credentials', 'tokens', 'api_keys', 'mcp-secrets',
]
def _safe_ignore_patterns(src_dir: Path, patterns: list[str]):
standard_ignore = shutil.ignore_patterns(*patterns)
def _ignore(path, names):
ignored = set(standard_ignore(path, names))
for name in names:
full_path = Path(path) / name
if full_path.is_symlink():
try:
resolved = full_path.resolve()
resolved.relative_to(src_dir.resolve())
except ValueError:
ignored.add(name)
return list(ignored)
return _ignore |
||
|
|
||
|
|
||
| def _ignore_func(*patterns): | ||
| """Create a callable that returns a list of filenames to ignore.""" | ||
| 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 | ||
| return _ignore | ||
|
|
||
| from rich.console import Console | ||
|
|
||
| from .base import SourceWithSkills | ||
|
|
@@ -91,11 +74,16 @@ def do_git_publish( | |
| for src_path, dest_name in items: | ||
| dest = items_dir / dest_name | ||
| if src_path.is_dir(): | ||
| shutil.copytree(src_path, dest, dirs_exist_ok=True, | ||
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | ||
| shutil.copytree( | ||
| src_path, | ||
| dest, | ||
| dirs_exist_ok=True, | ||
| ignore=shutil.ignore_patterns(*DEFAULT_IGNORE_PATTERNS), | ||
| symlinks=True, | ||
| ) | ||
|
Comment on lines
+77
to
+83
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the new shutil.copytree(
src_path,
dest,
dirs_exist_ok=True,
ignore=_safe_ignore_patterns(src_path, 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| # Generate README | ||
| readme_generator(items_dir, items, repo) | ||
|
|
@@ -162,11 +150,16 @@ def publish_all( | |
| dest_name = f"{source_id}/{skill_name}" | ||
| dest = skills_dir / dest_name | ||
| if skill.path.is_dir(): | ||
| shutil.copytree(skill.path, dest, dirs_exist_ok=True, | ||
| ignore=_ignore_func(*DEFAULT_IGNORE_PATTERNS)) | ||
| shutil.copytree( | ||
| skill.path, | ||
| dest, | ||
| dirs_exist_ok=True, | ||
| ignore=shutil.ignore_patterns(*DEFAULT_IGNORE_PATTERNS), | ||
| symlinks=True, | ||
| ) | ||
|
Comment on lines
+153
to
+159
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| else: | ||
| dest.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copy2(skill.path, dest) | ||
| shutil.copy2(skill.path, dest, follow_symlinks=False) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| skills_to_publish.append((skill.path, dest_name)) | ||
| break | ||
|
|
||
|
|
@@ -188,7 +181,7 @@ def publish_all( | |
| agent = all_agents.get(agent_name) | ||
| if agent: | ||
| dest = agents_dir / f"{agent_name}.md" | ||
| shutil.copy2(Path(agent.path), dest) | ||
| shutil.copy2(Path(agent.path), dest, follow_symlinks=False) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| agents_to_publish.append((Path(agent.path), f"agents/{agent_name}.md")) | ||
|
|
||
| if agents_to_publish: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
follow_symlinks=Falseonshutil.copy2will copy the symbolic link itself rather than the target file's content. This introduces several issues:/Users/username/projects/...) is committed to the public repository, leaking the developer's local username and directory structure.shutil.copy2withfollow_symlinks=Falsecan fail withOSErrorif the user does not have symlink creation privileges.Since the user explicitly selected this agent to publish, we should follow the symlink (the default behavior of
shutil.copy2) to ensure the actual content of the agent is copied and published.