T09 · Insecure Skill Coding Practices
- Location
bubble_plot.py:226- Finding
Path Traversal Through Untrusted Output Filenames
- Content
View full analysis
Vulnerability Details
File Location:
bubble_plot.py, lines 226–236
Vulnerability Type: Path traversal and arbitrary file overwrite
Risk Level: Highpython base = f"{gene}_dotplot" if label: base += f"_{label}" png_path = os.path.join(args.outdir, f"{base}.png") pdf_path = os.path.join(args.outdir, f"{base}.pdf") fig.savefig(png_path, dpi=args.dpi, bbox_inches='tight', facecolor='white') fig.savefig(pdf_path, bbox_inches='tight', facecolor='white') plt.close(fig) files_written.extend([png_path, pdf_path])Technical Analysis
The output filename is built directly from
geneandlabelwithout filename sanitization or path-containment validation.The
genevalue originates either from command-line arguments or from column names in an untrusted CSV/TSV file. Thelabelvalue originates from the tissue column in that file. Both can therefore contain absolute paths, path separators, or parent-directory components such as../.Passing the constructed value to
os.path.join()does not enforce containment withinargs.outdir. In particular:- An absolute
genevalue causesos.path.join()to discard the output-directory prefix. - Relative traversal components can resolve outside the output directory.
matplotlib.figure.Figure.savefig()overwrites an existing writable destination without requiring confirmation.
Exploitation requires the target parent directory to exist and the process to have write permission. The generated content remains a PNG or PDF plot, which limits the attacker to files using those appended extensions, but it does not prevent unauthorized creation or replacement of such files.
Attack Path
- An attacker supplies a CSV/TSV expression file containing a numeric gene column with a crafted name such as
../../shared/report, or persuades the o ...[truncated 1138 chars]
- An absolute
- Remediation
View remediation
Remediation Suggestions
- Convert every untrusted filename component to a safe basename using a strict allowlist, such as ASCII letters, digits, periods, underscores, and hyphens.
- Reject absolute paths,
.and..components, directory separators, null bytes, and platform-specific path separators. - Resolve the output directory and every candidate destination with
pathlib.Path.resolve(), then verify that the destination remains beneath the resolved output directory. - Generate internal identifiers for output filenames instead of using raw dataset labels. Maintain a separate mapping from identifiers to display labels if necessary.
- Avoid silently overwriting existing files. Use exclusive creation, generate a unique filename, or require explicit overwrite consent.
- Apply length limits to filename components to prevent filesystem errors and denial-of-service conditions.
Example containment check:
python from pathlib import Path import re def safe_component(value): value = re.sub(r"[^A-Za-z0-9._-]+", "_", str(value)) value = value.strip("._") if not value: raise ValueError("Unsafe or empty filename component") return value[:128] output_root = Path(args.outdir).resolve() base = safe_component(gene) + "_dotplot" if label: base += "_" + safe_component(label) png_path = (output_root / f"{base}.png").resolve() pdf_path = (output_root / f"{base}.pdf").resolve() if output_root not in png_path.parents or output_root not in pdf_path.parents: raise ValueError("Output path escapes the configured directory")
