{"record":{"id":"98f42ba0008ccac2","repo":"libnyanpasu/clash-nyanpasu","slug":"refusing-to-write-through-unexpected-symlink-or-re","errorCode":null,"errorMessage":"refusing to write through unexpected symlink or reparse point at {}","messagePattern":"refusing to write through unexpected symlink or reparse point at (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"backend/tauri/src/service/profile_file.rs","lineNumber":1556,"sourceCode":"        AtomicFile::new(&full, OverwriteBehavior::AllowOverwrite)\n            .write(|file| file.write_all(content.as_bytes()))\n            .with_context(|| format!(\"atomic write {}\", full.display()))\n    }\n\n    fn remove(&self, path: &ManagedProfilePath) -> anyhow::Result<()> {\n        let full = self.resolve(path)?;\n        Self::remove_nofollow(&full)\n    }\n\n    fn read_external(&self, target: &ExternalProfilePath) -> anyhow::Result<String> {\n        std::fs::read_to_string(target.as_path())\n            .with_context(|| format!(\"read external profile target {target}\"))\n    }\n\n    fn ensure_not_symlink(&self, path: &ManagedProfilePath) -> anyhow::Result<()> {\n        let full = self.resolve(path)?;\n        match std::fs::symlink_metadata(&full) {\n            Ok(meta) if is_symlink_or_reparse(&meta) => bail!(\n                \"refusing to write through unexpected symlink or reparse point at {}\",\n                full.display()\n            ),\n            Ok(_) => Ok(()),\n            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),\n            Err(e) => Err(e).with_context(|| format!(\"inspect profile file {}\", full.display())),\n        }\n    }\n\n    fn ensure_symlink(\n        &self,\n        path: &ManagedProfilePath,\n        target: &ExternalProfilePath,\n    ) -> anyhow::Result<()> {\n        let full = self.resolve(path)?;\n        self.ensure_managed_parent(&full)?;\n        match std::fs::symlink_metadata(&full) {\n            Ok(meta) if meta.file_type().is_symlink() => {","sourceCodeStart":1538,"sourceCodeEnd":1574,"githubUrl":"https://github.com/libnyanpasu/clash-nyanpasu/blob/f7dbce2997c633e484f54788035e770b3ee99773/backend/tauri/src/service/profile_file.rs#L1538-L1574","documentation":"Before writing a managed profile file, ensure_not_symlink resolves the managed path and checks the on-disk entry with symlink_metadata. If it exists as a symlink or Windows reparse point, the write is refused: writing through a symlink would modify a file outside the managed profiles root, which may be an attack (symlink planting) or accidental misconfiguration. Only plain files (or non-existent paths, which are created fresh) are acceptable.","triggerScenarios":"Saving/updating a managed profile whose target path on disk has been replaced by a symlink or reparse point — e.g. a user symlinked the profile file into another location, a sync tool created placeholders, or an attacker planted a symlink pointing at a sensitive file to trick the app into overwriting it.","commonSituations":"Users replacing profile files with symlinks to keep them in a dotfiles repo or cloud folder; OneDrive 'files on demand' reparse points on Windows; malicious symlink placement in shared/multi-user profile directories; leftover junctions after moving the profiles directory.","solutions":["Replace the symlink/reparse point at the reported path with a real regular file (copy the link target's content back if needed), then retry the write.","Move the profiles root to a location not managed by cloud-sync placeholder features, or mark the folder 'always keep on this device'.","If the symlink was intentional (dotfiles management), stop managing that file through the library and maintain it externally.","In multi-user environments, restrict write access to the profiles directory to the running user to prevent symlink planting."],"exampleFix":"// before: write refuses through the user's symlink\nservice.write_profile(&path, contents).await?;\n// after: replace the symlink with a real file first\nlet full = resolve(path);\nlet meta = std::fs::symlink_metadata(&full)?;\nif meta.is_symlink() {\n    let target = std::fs::read_link(&full)?;\n    std::fs::remove_file(&full)?;\n    std::fs::copy(target, &full)?;\n}\nservice.write_profile(&path, contents).await?;","handlingStrategy":"validation","validationCode":"fn writable_regular_target(resolve: impl Fn(&ManagedProfilePath) -> std::io::Result<PathBuf>, path: &ManagedProfilePath) -> Result<bool, std::io::Error> {\n    match std::fs::symlink_metadata(resolve(path)?) {\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(true),\n        Err(e) => Err(e),\n        Ok(m) => Ok(!m.is_symlink()),\n    }\n}","typeGuard":"fn is_symlink_or_reparse(m: &std::fs::Metadata) -> bool {\n    m.is_symlink()\n        || m.file_attributes()\n            .map(|a| a & 0x400 != 0) // FILE_ATTRIBUTE_REPARSE_POINT\n            .unwrap_or(false)\n}","tryCatchPattern":"match service.write_profile(&path, contents).await {\n    Err(e) if e.to_string().contains(\"refusing to write through unexpected symlink\") => {\n        // surface to the user: replace the symlink with a real file, then retry\n    }\n    other => other?,\n}","preventionTips":["Do not symlink managed profile files into dotfiles/cloud folders","Keep profile directories outside OneDrive/Dropbox placeholder management","Restrict write access to the profiles directory to the running user","Replace intentional symlinks with hard copies before handing the path to the library"],"tags":["security","symlink","filesystem","write-guard"],"backgroundTag":"path-traversal-blocked","analyzedSha":"f7dbce2997c633e484f54788035e770b3ee99773","analyzedAt":"2026-09-08T01:24:59.197Z","contentChangedAt":"2026-09-08T01:24:59.197Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}