T09 · Insecure Skill Coding Practices
- Location
scripts/netlify_zip_deploy.py:32- Finding
Deployment Archive Follows Symbolic Links and May Exfiltrate Files Outside the Site Directory
- Content
View full analysis
Vulnerability Details
File Location:
scripts/netlify_zip_deploy.py, lines 32-41
Vulnerability Type: Symbolic-link traversal during deployment archive creation
Risk Level: MediumVulnerable Code
python def build_zip(source_dir: Path) -> Path: tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False) tmp.close() zip_path = Path(tmp.name) with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive: for path in sorted(source_dir.rglob("*")): if path.is_file(): archive.write(path, path.relative_to(source_dir)) return zip_pathThe resulting archive is subsequently transmitted to Netlify:
python def create_deploy(site_id: str, token: str, zip_bytes: bytes, draft: bool) -> dict: url = f"{API_ROOT}/sites/{site_id}/deploys" if draft: url += "?draft=true" return request("POST", url, token, zip_bytes, "application/zip")Technical Analysis
Path.is_file()follows symbolic links. If a symbolic link beneath the selectedsource_dirpoints to a regular file outside that directory, the condition evaluates to true.ZipFile.write()then opens the symlink path and archives the contents of its external target.Although
path.relative_to(source_dir)controls the name recorded inside the ZIP, it does not verify that the resolved source file remains withinsource_dir. Consequently, the intended filesystem boundary is not enforced.This behavior is particularly risky when the deployment tree was cloned, extracted, generated, or otherwise influenced by an untrusted party. The outbound upload itself is necessary for the declared Netlify deployment functionality, and the destination is the fixed official Netlify API. However, the set of files uploaded can exceed the selected deployment directory's legitimate contents.
Attack Path
- An attacker supplies or influences a site directory t ...[truncated 1698 chars]
- Remediation
View remediation
Remediation Suggestions
Reject symbolic links and verify that every resolved input file remains beneath the resolved deployment root before adding it to the archive:
python def build_zip(source_dir: Path) -> Path: source_root = source_dir.resolve(strict=True) tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False) tmp.close() zip_path = Path(tmp.name) try: with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as archive: for path in sorted(source_root.rglob("*")): if path.is_symlink(): raise ValueError( f"Refusing to archive symbolic link: {path}" ) if not path.is_file(): continue resolved_path = path.resolve(strict=True) if not resolved_path.is_relative_to(source_root): raise ValueError( f"File resolves outside deployment root: {path}" ) archive.write( resolved_path, resolved_path.relative_to(source_root), ) return zip_path except Exception: zip_path.unlink(missing_ok=True) raiseAdditional hardening should include:
- Apply a deployment allowlist or explicit exclusion rules for
.env, private keys, VCS metadata, backup files, and.website-managermetadata. - Display or record a manifest of files before upload so the operator can review the exact deployment contents.
- Refuse special filesystem objects, including sockets, devices, and named pipes.
- Create the temporary archive with restrictive permissions and ensure cleanup on every error path.
- Prefer draft deployments when processing site trees that have not been reviewed.
- Document that cloned, extracted, or third-party site directories must be treated as ...[truncated 70 chars]
- Apply a deployment allowlist or explicit exclusion rules for
