{"record":{"id":"53df430696468cb7","repo":"quickwit-oss/quickwit","slug":"path-is-forbidden-only-simple-relative-path","errorCode":null,"errorMessage":"path `{}` is forbidden. only simple relative path are allowed","messagePattern":"path `(.+?)` is forbidden\\. only simple relative path are allowed","errorType":"exception","errorClass":"StorageError","httpStatus":null,"severity":"error","filePath":"quickwit/quickwit-storage/src/local_file_storage.rs","lineNumber":106,"sourceCode":"\n    async fn delete_single_file(&self, relative_path: &Path) -> StorageResult<()> {\n        let full_path = self.full_path(relative_path)?;\n        ignore_error_kind!(ErrorKind::NotFound, tokio::fs::remove_file(full_path).await)?;\n        Ok(())\n    }\n}\n\n/// Ensure that the path given does not include any \"..\" for security reasons.\n///\n/// In order to reduce the attack surface, we want to make sure the `FileStorage`\n/// only access/delete files that are children of its root_directory.\nfn ensure_valid_relative_path(path: &Path) -> StorageResult<()> {\n    for component in path.components() {\n        match component {\n            Component::RootDir | Component::ParentDir | Component::Prefix(_) => {\n                // We forbid `Path` components that are breaking the assumption that\n                // root.join(path) is a child of root (if we omit fs links).\n                return Err(StorageErrorKind::Unauthorized.with_error(anyhow::anyhow!(\n                    \"path `{}` is forbidden. only simple relative path are allowed\",\n                    path.display()\n                )));\n            }\n            Component::CurDir | Component::Normal(_) => {\n                // we accept `./` and subdir/\n            }\n        }\n    }\n    Ok(())\n}\n\n/// Delete empty directories starting from `{root}/{path}` directory and stopping at `{root}`\n/// directory. Note that the `{root}` directory is not deleted.\nfn delete_all_dirs_if_empty<'a>(\n    root: &'a Path,\n    path: &'a Path,\n) -> BoxFuture<'a, std::io::Result<()>> {","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/quickwit-oss/quickwit/blob/a39730c5cdcd1a4fe798403737ae293999ea21f8/quickwit/quickwit-storage/src/local_file_storage.rs#L88-L124","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet path = Path::from(\"/data/quickwit/indexes/my-index/manifest\");\nstorage.put(&path, payload).await?;\n// after\nlet root = Path::from(\"/data/quickwit\");\nlet relative = path.strip_prefix(&root).unwrap_or(&path);\nstorage.put(relative, payload).await?;","handlingStrategy":"validation","validationCode":"fn is_safe_relative_path(path: &Path) -> bool {\n    use std::path::Component;\n    path.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir))\n}","typeGuard":"fn safe_rel_path(path: &Path) -> Option<&Path> {\n    use std::path::Component;\n    if path.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir)) {\n        Some(path)\n    } else {\n        None\n    }\n}","tryCatchPattern":"match storage.put(&path, payload).await {\n    Err(e) if e.kind() == quickwit_storage::StorageErrorKind::Unauthorized => {\n        anyhow::bail!(\"key `{path}` escapes storage root; strip the root prefix first\");\n    }\n    other => other,\n}","preventionTips":["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 '..'."],"tags":["storage","security","path-traversal","local-filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"a39730c5cdcd1a4fe798403737ae293999ea21f8","analyzedAt":"2026-09-08T13:19:37.784Z","contentChangedAt":"2026-09-08T13:19:37.784Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}