PathValidator¶
Purpose¶
PathValidator is a security component responsible for ensuring that a filesystem path resolves within an explicitly permitted base directory.
It resolves candidate paths before checking containment, preventing relative path traversal and symlink-based paths from escaping the permitted boundary.
PathValidator is intentionally small and independent of ETLRelay's storage, source, sink, reader, and writer implementations. LocalFileSystem uses it to validate local filesystem paths before performing I/O.
Path validation does not require the candidate to exist. Filesystem existence and access errors are handled by the storage component.
Class Diagram¶
classDiagram
class PathValidator {
-base_path: Path
+validate(path: Path | str) Path
}
class LocalFileSystem {
-path_validator: PathValidator
}
LocalFileSystem --> PathValidator : validates paths with
Responsibilities¶
PathValidator¶
PathValidator is responsible for:
- Accepting an optional base directory during construction.
- Defaulting the base directory to the current working directory when none is provided.
- Resolving the base directory and candidate paths.
- Determining whether a resolved candidate remains within the permitted base directory.
- Returning the resolved path when valid.
- Raising
PathValidationErrorwhen a candidate escapes the permitted boundary.
PathValidator is not responsible for:
- Checking whether a candidate path exists.
- Opening, reading, or writing files.
- Creating directories.
- Parsing CSV or other data formats.
- Knowing about
Batch. - Knowing about
Source,Sink, or pipeline execution. - Performing authorization or user-level access control.
- Logging application-level events.
Dependency Relationship¶
LocalFileSystem
│
▼
PathValidator
LocalFileSystem depends on PathValidator to enforce local path containment before performing filesystem operations.
PathValidator does not depend on LocalFileSystem or any higher-level ETLRelay component. It uses Python's pathlib.Path for path resolution and containment checks.
FileSource and FileSink depend on the FileSystem protocol rather than on PathValidator directly. Path validation is handled by the concrete local storage implementation.
Interface¶
from pathlib import Path
class PathValidator:
def __init__(self, base_path: Path | None = None) -> None:
...
def validate(self, path: Path | str) -> Path:
...
class PathValidationError(Exception):
"""Raised when a filesystem path fails validation."""
Contract¶
| Operation | Expected behavior |
|---|---|
| Construct without a base path | Uses the resolved current working directory |
| Construct with an explicit base path | Uses the resolved supplied directory as the permitted boundary |
| Validate a contained relative path | Returns the resolved path |
| Validate a contained absolute path | Returns the resolved path |
| Validate the base directory itself | Returns the resolved base path |
Validate a path containing safe . or .. segments |
Returns the resolved path if it remains within the boundary |
| Validate a path containing traversal that escapes the base | Raises PathValidationError |
| Validate an absolute path outside the base | Raises PathValidationError |
| Validate a path with a similar directory-name prefix outside the base | Raises PathValidationError |
| Validate a symlink resolving inside the base | Returns the resolved target path |
| Validate a symlink resolving outside the base | Raises PathValidationError |
| Validate a nonexistent path inside the base | Returns the resolved path; existence is not checked |
SOLID Considerations¶
Single Responsibility Principle¶
PathValidator has one reason to change: the rules governing local filesystem path containment.
It does not combine path validation with filesystem I/O, data-format handling, or application logic.
Open/Closed Principle¶
The implementation remains deliberately small. Additional path policies should be introduced only when concrete requirements justify them.
No path-policy hierarchy or configurable validation framework is required.
Liskov Substitution Principle¶
No inheritance hierarchy is required. PathValidator is a concrete security utility whose behavior is defined by its contract.
Interface Segregation Principle¶
No protocol is necessary for the current implementation. PathValidator exposes only the operation its consumer requires: validate().
Dependency Inversion Principle¶
LocalFileSystem delegates path-containment checks to PathValidator rather than implementing its own validation logic.
PathValidator depends only on Python's standard path-handling functionality and remains independent of higher-level ETLRelay components.
Design Constraints¶
The implementation should use pathlib.Path and explicit path containment rather than string-prefix comparisons.
The validator establishes its permitted boundary during construction:
PathValidator(base_path)
│
▼
trusted boundary
│
▼
validate(candidate_path)
│
├── contained ──> resolved Path
│
└── outside ────> PathValidationError
The containment check must occur after path resolution so that traversal segments and symlinks are evaluated against their resolved locations.
Path existence is deliberately outside this component's contract:
PathValidator
│
├── contained path ──> resolved Path
│
└── escaping path ───> reject
LocalFileSystem
│
├── open existing resource for reading
│
└── open destination for writing
PathValidator establishes a path-containment boundary; it is not a complete filesystem sandbox or authorization mechanism. Filesystem changes between validation and subsequent use can still create time-of-check-to-time-of-use risks.
No additional abstractions such as PathPolicy, PathResolver, or a filesystem interface are required for this component.