affaan-m/ECC · error · ValueError

output path contains an invalid component

Error message

output path contains an invalid component

What it means

_SafeOutput._open_dir walks the output tree component by component using dir_fd-relative operations, and validates each component name. A component that is empty, is '.' or '..', or contains '/' (path-separator injection) is rejected to guarantee the final path cannot escape the anchored root directory. This is a path-traversal guard for every prepared/written artifact.

Solutions

  1. Sanitize components: reject/split any name containing '/', and drop '', '.', '..' segments before calling the API
  2. Use pathlib to derive pure relative names: name = Path(untrusted).name, then pass parts individually
  3. Never pass absolute paths into write_json/prepare — the root is the anchor; pass paths relative to it
  4. Validate user-supplied artifact names against a strict pattern (e.g. ^[A-Za-z0-9._-]+$)

Example fix

# before
safe.write_json(user_supplied_relpath, data)  # e.g. '../etc/evil.json'

# after
parts = tuple(p for p in PurePosixPath(user_supplied_relpath).parts
              if p not in ('', '.', '..') and '/' not in p)
if not parts or any(p.startswith('/') for p in parts):
    raise ValueError('unsafe artifact path')
safe.write_json(parts, data)
Defensive patterns

Strategy: validation

Validate before calling

import re
SAFE_COMPONENT = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$')
def safe_parts(relpath):
    parts = [p for p in PurePosixPath(relpath).parts]
    if not parts or any(not SAFE_COMPONENT.match(p) for p in parts):
        raise ValueError(f'unsafe artifact path: {relpath!r}')
    return tuple(parts)

Type guard

def is_safe_component(p: str) -> bool:
    return bool(p) and p not in ('.', '..') and '/' not in p and not p.startswith('/')

Try / catch

try:
    safe.write_json(parts, data)
except ValueError as e:
    if 'invalid component' in str(e):
        print(f'Rejecting untrusted path: {parts!r} ({e})')
    else:
        raise

Prevention

When it happens

Trigger: Calling prepare/write_json/artifact_metadata with a relative path containing '../', a leading '/', an embedded '/', or an empty segment (e.g. 'out//file.json', './x', 'sub/../file.json').

Common situations: Building paths from user input or untrusted metadata (filenames from probe data); joining an absolute path with the output root and passing the concatenation; empty filename variables from failed templates.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/679781021384571a. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-application/scripts/tasteforge/workflow.py:202

            root.mkdir(mode=0o700)
        self.root = root
        self._root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
        self._written: list[str] = []

    def close(self) -> None:
        if self._root_fd >= 0:
            os.close(self._root_fd)
            self._root_fd = -1

    def __del__(self) -> None:
        self.close()

    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:
        current = os.dup(self._root_fd)
        try:
            for part in parts:
                if not part or part in {".", ".."} or "/" in part:
                    raise ValueError("output path contains an invalid component")
                try:
                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
                except FileNotFoundError:
                    if not create:
                        raise ValueError(f"missing output directory: {part}") from None
                    os.mkdir(part, mode=0o700, dir_fd=current)
                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)
                if stat.S_ISLNK(metadata.st_mode):
                    raise ValueError(f"output directory must not be a symlink: {part}")
                if not stat.S_ISDIR(metadata.st_mode):
                    raise ValueError(f"output intermediate must be a directory: {part}")
                child = os.open(
                    part,
                    os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
                    dir_fd=current,
                )
                os.close(current)
                current = child

View on GitHub (pinned to 8321021c54)