T09 · Insecure Skill Coding Practices
- Location
scripts/deploy.py:101- Finding
Site Upload Can Follow File Symlinks and Publicly Expose Data Outside the Selected Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/deploy.py:101-105, 112-120
Vulnerability Type: Unrestricted file traversal through symbolic links
Risk Level: HighVulnerable Code
python def upload_directory(owner, repo, dir_path, branch, token): """Incrementally upload a whole directory via the contents API. Creates/updates files present locally while leaving any remote files that are not in the local directory untouched. This is a per-file sync (no `git push --force`), so it can never wipe out pre-existing remote content. """ count = 0 for root, dirs, files in os.walk(dir_path): dirs[:] = [d for d in dirs if d != ".git"] for fname in files: local_path = os.path.join(root, fname) remote_path = os.path.relpath(local_path, dir_path).replace(os.sep, "/") upload_single_file(owner, repo, local_path, remote_path, branch, token) count += 1 print("[OK] Uploaded {} file(s)".format(count))python def upload_single_file(owner, repo, local_file, remote_path, branch, token): """Upload one file to an existing repo via the contents API (no git needed). Auto-handles update-vs-create: fetches the file's SHA first if it exists. """ with open(local_file, "rb") as f: raw = f.read() payload = { "message": "Add " + remote_path, "content": base64.b64encode(raw).decode("ascii"), "branch": branch, }Technical Analysis
The directory uploader recursively selects every file other than content under a directory named
.git. It does not reject symbolic-link files, inspect each file withlstat(), or verify that each resolved file path remains under the resolved deployment root.Although
os.walk()does not follow directory links by default, a symbolic link to a regular file still appears infiles. The subsequentopen(local_file, "rb")follows that link and reads its target. The target may th ...[truncated 2230 chars]- Remediation
View remediation
Remediation Suggestions
- Resolve the deployment root once with
os.path.realpath()orpathlib.Path.resolve(). - Inspect every candidate with
os.lstat()orPath.is_symlink()and reject symbolic links by default. - Resolve every candidate file and verify containment using
os.path.commonpath()rather than string-prefix checks. - Reject any file whose resolved path is outside the resolved deployment root.
- Add deny-by-default exclusions for sensitive names and patterns, including
.env,.env.*, private-key extensions, credential files,.git, and platform-specific secret stores. - Require explicit confirmation before uploading hidden files or files matching secret-detection patterns.
- Display a complete upload manifest before network transmission, including warnings for hidden, unusually large, or potentially sensitive files.
- Apply the same resolved-path validation to
--file, since a directly supplied file path may also be a symlink. - Add automated tests covering file symlinks, directory symlinks, relative links, broken links, links escaping through parent directories, and normal in-root files.
A suitable containment check should follow this pattern:
python site_root = os.path.realpath(dir_path) candidate = os.path.join(root, fname) if os.path.islink(candidate): raise ValueError("Symbolic links are not allowed: " + candidate) resolved = os.path.realpath(candidate) if os.path.commonpath([site_root, resolved]) != site_root: raise ValueError("File resolves outside deployment root: " + candidate)For stronger assurance, open files with platform-supported no-follow semantics, such as
os.O_NOFOLLOW, to reduce time-of-check/time-of-use risks where available.- Resolve the deployment root once with
