T09 · Insecure Skill Coding Practices
- Location
scripts/generate_portal.py:844- Finding
Ineffective Output-Directory Symlink Validation
- Content
View full analysis
Vulnerability Details
File Location:
scripts/generate_portal.py, lines 844–870
Vulnerability Type: Symlink-following file-write weakness
Risk Level: MediumVulnerable Code
python def prepare_output(path: Path, pages: set[str], overwrite: bool) -> Path: output = path.expanduser().resolve() if output.parent == output: fail("Refusing to use a filesystem root as the output directory") if output.exists() and output.is_symlink(): fail("Output directory must not be a symbolic link") if output.exists() and not output.is_dir(): fail("Output path exists and is not a directory") if output.exists(): contents = list(output.iterdir()) if contents and not overwrite: fail("Output directory is not empty; use --overwrite to replace expected portal pages") for name in pages: candidate = output / name if candidate.exists() and (candidate.is_symlink() or not candidate.is_file()): fail(f"Expected page path is not a regular file: {candidate}") else: output.mkdir(parents=True) return output def write_pages(output: Path, pages: dict[str, str]) -> None: for name, content in pages.items(): if Path(name).name != name or not name.endswith(".html"): fail(f"Unsafe generated filename: {name}") destination = output / name destination.write_text(content, encoding="utf-8")Technical Analysis
The output path is passed through
Path.resolve()before the code checksoutput.is_symlink(). Resolution dereferences a symbolic link and returns its target, so the subsequent symlink check examines the target directory rather than the path originally supplied by the user. As a result, the intended rejection of a symlinked output directory is ineffective.When
--overwriteis enabled, the renderer may replace generated filenames such asindex.html,patents.html, and company or technology ...[truncated 1619 chars]- Remediation
View remediation
Remediation Suggestions
- Inspect the original, unresolved output path using
lstat()orPath.is_symlink()before callingresolve(). - Validate every existing path component between a trusted base directory and the output directory, rejecting symbolic links where they are not explicitly permitted.
- Constrain output to an approved base directory and verify the resolved destination remains beneath that base with
Path.is_relative_to()or an equivalent robust containment check. - Use directory-descriptor-relative file operations with no-follow semantics, such as
openat-style operations withO_NOFOLLOW, where supported. - Open destination files with flags that reject symbolic links and unexpected file types, rather than relying only on a prior path check.
- Write to securely created temporary files in the same validated directory, flush them, and atomically replace expected regular files.
- Revalidate destination type immediately before replacement and minimize the interval between validation and writing.
- Add regression tests covering a symlinked output directory, symlinked parent components, symlinked destination files, and concurrent path replacement.
- Inspect the original, unresolved output path using
