quickwit-oss/quickwit · error · StorageError
path `{}` is forbidden. only simple relative path are allowe
Error message
path `{}` is forbidden. only simple relative path are allowed What it means
ensure_valid_relative_path rejects paths containing RootDir, ParentDir, or Windows Prefix components because root.join(path) must always stay inside the storage root. Such a path would escape the root directory (path traversal), so it is refused with StorageErrorKind::Unauthorized before any file operation.
Source
Thrown at quickwit/quickwit-storage/src/local_file_storage.rs:106
async fn delete_single_file(&self, relative_path: &Path) -> StorageResult<()> {
let full_path = self.full_path(relative_path)?;
ignore_error_kind!(ErrorKind::NotFound, tokio::fs::remove_file(full_path).await)?;
Ok(())
}
}
/// Ensure that the path given does not include any ".." for security reasons.
///
/// In order to reduce the attack surface, we want to make sure the `FileStorage`
/// only access/delete files that are children of its root_directory.
fn ensure_valid_relative_path(path: &Path) -> StorageResult<()> {
for component in path.components() {
match component {
Component::RootDir | Component::ParentDir | Component::Prefix(_) => {
// We forbid `Path` components that are breaking the assumption that
// root.join(path) is a child of root (if we omit fs links).
return Err(StorageErrorKind::Unauthorized.with_error(anyhow::anyhow!(
"path `{}` is forbidden. only simple relative path are allowed",
path.display()
)));
}
Component::CurDir | Component::Normal(_) => {
// we accept `./` and subdir/
}
}
}
Ok(())
}
/// Delete empty directories starting from `{root}/{path}` directory and stopping at `{root}`
/// directory. Note that the `{root}` directory is not deleted.
fn delete_all_dirs_if_empty<'a>(
root: &'a Path,
path: &'a Path,
) -> BoxFuture<'a, std::io::Result<()>> {View on GitHub (pinned to a39730c5cd)
Solutions
- Strip the storage root prefix from the path before handing it to LocalFileStorage so only the relative remainder is passed.
- Normalize the path (e.g. via path.strip_prefix(root) or Path::components filtering) to remove any '..' or leading '/' segments.
- If you truly need to reach outside the root, configure a second storage instance rooted at that location instead of traversing.
- Sanitize externally-supplied file keys at ingestion time, rejecting or rewriting any non-Normal component before persisting.
Example fix
// before
let path = Path::from("/data/quickwit/indexes/my-index/manifest");
storage.put(&path, payload).await?;
// after
let root = Path::from("/data/quickwit");
let relative = path.strip_prefix(&root).unwrap_or(&path);
storage.put(relative, payload).await?; Defensive patterns
Strategy: validation
Validate before calling
fn is_safe_relative_path(path: &Path) -> bool {
use std::path::Component;
path.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
} Type guard
fn safe_rel_path(path: &Path) -> Option<&Path> {
use std::path::Component;
if path.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir)) {
Some(path)
} else {
None
}
} Try / catch
match storage.put(&path, payload).await {
Err(e) if e.kind() == quickwit_storage::StorageErrorKind::Unauthorized => {
anyhow::bail!("key `{path}` escapes storage root; strip the root prefix first");
}
other => other,
} Prevention
- Strip the storage root prefix from absolute paths before every local storage call.
- Sanitize user/remote-supplied keys, rejecting '..' and absolute forms at ingestion.
- Use one storage per root; never traverse across roots via '..'.
When it happens
Trigger: Any LocalFileStorage operation (via full_path) given an absolute path (leading '/'), a path containing '..' (e.g. "../other/index/files"), or a Windows drive prefix. Typically triggered by storing raw user- or remote-supplied keys as local paths instead of sanitized relative keys.
Common situations: Configuring a storage root and then writing files keyed by absolute paths from another source; ingesting file paths from user input containing '../'; porting code that assumed absolute paths were accepted; tests using '/tmp/...' style paths against a rooted local storage.
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.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- no parent directory for {full_path:?}
- `append_records` should be called with `position_opt: None`
- failed to run mrecordlog operation
- stdin cannot be checkpointed
- Unexpected span kind: {}
AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08).
Data as JSON: /api/errors/53df430696468cb7.
Report an issue: GitHub.