{"record":{"id":"362a6bf22b35279e","repo":"neondatabase/neon","slug":"path-relative-path-is-not-relative","errorCode":null,"errorMessage":"Path {relative_path:?} is not relative","messagePattern":"Path (.+?) is not relative","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"libs/remote_storage/src/lib.rs","lineNumber":127,"sourceCode":"    where\n        D: serde::Deserializer<'de>,\n    {\n        let str = String::deserialize(deserializer)?;\n        Ok(Self(Utf8PathBuf::from(&str)))\n    }\n}\n\nimpl std::fmt::Display for RemotePath {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        std::fmt::Display::fmt(&self.0, f)\n    }\n}\n\nimpl RemotePath {\n    pub fn new(relative_path: &Utf8Path) -> anyhow::Result<Self> {\n        anyhow::ensure!(\n            relative_path.is_relative(),\n            \"Path {relative_path:?} is not relative\"\n        );\n        Ok(Self(relative_path.to_path_buf()))\n    }\n\n    pub fn from_string(relative_path: &str) -> anyhow::Result<Self> {\n        Self::new(Utf8Path::new(relative_path))\n    }\n\n    pub fn with_base(&self, base_path: &Utf8Path) -> Utf8PathBuf {\n        base_path.join(&self.0)\n    }\n\n    pub fn object_name(&self) -> Option<&str> {\n        self.0.file_name()\n    }\n\n    pub fn join(&self, path: impl AsRef<Utf8Path>) -> Self {\n        Self(self.0.join(path))","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/libs/remote_storage/src/lib.rs#L109-L145","documentation":"RemotePath is the library's typed relative path into a remote bucket or local storage root; RemotePath::new enforces relativity via Utf8Path::is_relative() and rejects anything absolute — a leading '/', a Windows drive prefix, or any rooted path. This is input validation: the path is later joined onto a base directory/bucket prefix, so an absolute component would escape or corrupt the join.","triggerScenarios":"Constructing RemotePath from user input or URL fragments that start with '/'; passing a fully-qualified local path; formatting paths with a leading separator (format!(\\\"/{}/file\\\", tenant)); on Windows, paths with a drive letter or UNC prefix.","commonSituations":"Parsing request paths off an HTTP API and forwarding them verbatim; joining components via string concatenation with slashes; porting code that historically stored absolute paths; double-prefixed paths from config (base + '/absolute').","solutions":["Strip leading separators before constructing: path.trim_start_matches('/')","Build paths by joining components (Utf8PathBuf::push / join) rather than string concatenation","Validate and normalize paths at the API boundary before they reach storage code","On Windows also reject drive prefixes and UNC paths; treat a leading '/' as the canonical bad case"],"exampleFix":"// before: leading slash from user input makes is_relative() false\nlet path = RemotePath::from_string(&format!(\"/{}/{}\", tenant_id, file_name))?;\n\n// after: normalize before constructing\nlet relative = format!(\"{}/{}\", tenant_id, file_name);\nlet path = RemotePath::from_string(relative.trim_start_matches('/'))?;","handlingStrategy":"validation","validationCode":"// Normalize and validate before constructing a RemotePath.\nfn to_remote_path(raw: &str) -> anyhow::Result<RemotePath> {\n    let normalized = raw.trim_start_matches('/');\n    anyhow::ensure!(!normalized.is_empty(), \"path {raw:?} is empty after normalization\");\n    RemotePath::from_string(normalized)\n}","typeGuard":"fn is_valid_remote_path(raw: &str) -> bool {\n    let p = Utf8Path::new(raw.trim_start_matches('/'));\n    !raw.starts_with(|c: char| c.is_ascii_alphabetic() && raw[1..].starts_with(':')) // windows drive\n        && p.is_relative()\n        && !p.has_root()\n        && p.components().next().is_some()\n        && !p.components().any(|c| matches!(c, std::path::Component::ParentDir))\n}","tryCatchPattern":"// Construct paths via a single validated entry point; catch and reject with a clear message.\npub fn parse_user_path(raw: &str) -> Result<RemotePath, String> {\n    RemotePath::from_string(raw.trim_start_matches('/'))\n        .map_err(|e| format!(\"invalid storage path {raw:?}: {e}; paths must be relative, no leading '/'\"))\n}","preventionTips":["Never build storage paths by string concatenation with '/' — join components with path APIs","Reject or normalize absolute paths at the HTTP/API boundary before they reach storage code","Add unit tests covering leading '/', '..' components, empty strings, and Windows drive prefixes"],"tags":["rust","path-validation","input-validation","cross-platform"],"backgroundTag":"absolute-path-rejected","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}