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

  1. Strip the storage root prefix from the path before handing it to LocalFileStorage so only the relative remainder is passed.
  2. Normalize the path (e.g. via path.strip_prefix(root) or Path::components filtering) to remove any '..' or leading '/' segments.
  3. If you truly need to reach outside the root, configure a second storage instance rooted at that location instead of traversing.
  4. 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

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.

Related errors


AI-assisted analysis of quickwit-oss/quickwit@a39730c5cd (2026-09-08). Data as JSON: /api/errors/53df430696468cb7. Report an issue: GitHub.