marshal() backup write follows symlinks on predictable users.backup.json path (CWE-59 confused-deputy overwrite)
low Possibly Valid medium confidence
Status
Possibly Valid
The symlink-following write on a predictable path is a real, fully-read code weakness (os.Create without O_NOFOLLOW/O_EXCL on the fixed users.backup.json). possibly_valid because the confused-deputy privilege crossing depends on an unresolved deployment assumption — an attacker-writable CWD combined with a more-privileged filebrowser process — which cannot be established from source alone.
Repository / Component
Plain-English Description
When importing users with the --replace option, the tool writes a backup to a fixed, predictable filename in the current directory using an open call that follows symbolic links. If someone pre-plants a symlink there pointing at another file, running the command overwrites and destroys that other file.
Description of the Underlying Issue
marshal() (cmd/utils.go:202-220) opens the output with os.Create(filename) at :203, i.e. OpenFile(O_RDWR|O_CREATE|O_TRUNC, 0666) — no O_NOFOLLOW, no O_EXCL, and no Lstat pre-check. If the target path is an existing symlink, os.Create follows it and truncates the symlink's target. The destructive `filebrowser users import --replace` path (cmd/users_import.go:54-63) calls marshal with the hardcoded name users.backup.json in the process CWD before deleting the existing user base. Other callers (cmd/users_export.go, cmd/config_export.go) pass operator-supplied paths, so users.backup.json is the uniquely predictable, pre-plantable target. The broken invariant: writing to a fixed, predictable path must not follow attacker-controllable symlinks with the process's privileges (CWE-59).
Potential Attack
In a directory the attacker can write to (but where the victim file itself is not directly attacker-writable), the attacker pre-plants symlink users.backup.json -> filebrowser.db (or any file writable by the filebrowser process). An operator later runs `filebrowser users import --replace new.json` from that directory; marshal()'s os.Create follows the symlink and truncates the target, then writes user JSON into it.
Outcomes of Potential Attack
The attacker destroys or corrupts a file the filebrowser process can write but they cannot directly — e.g. truncating filebrowser.db (data loss / denial of service) or clobbering another sensitive file, with the privileges of the filebrowser process. This is a classic confused-deputy overwrite; it does not directly grant read access to the target.
Affected Scope
cmd/utils.go:202-220 marshal() opens output with os.Create (no O_NOFOLLOW/O_EXCL); invoked with the fixed, attacker-predictable name users.backup.json in the process CWD at cmd/users_import.go:60 during `filebrowser users import --replace`.
Suggested Fix (plain english)
Open the backup file in a way that refuses to follow symlinks and fails if the file already exists, and write backups to a directory attackers cannot write to.
Suggested Fix (detailed)
In marshal() (cmd/utils.go:203) open output with O_CREATE|O_EXCL (and O_NOFOLLOW where the OS supports it), or Lstat the path and refuse if it is a symlink. Prefer os.CreateTemp with 0600 followed by an atomic rename. Write backups to a non-CWD, non-attacker-writable directory and fix the fixed-name call site at cmd/users_import.go:60. Verify that a pre-existing symlink at the backup path causes the write to fail rather than truncating its target. In marshal(), open output with O_CREATE|O_EXCL (and O_NOFOLLOW where available) or Lstat-and-refuse symlinks; prefer os.CreateTemp with 0600 then atomic rename; write backups to a non-CWD, non-attacker-writable directory. Fix at cmd/utils.go:203 and the fixed-name call at cmd/users_import.go:60.
Validation
The symlink-following write on a predictable path is a real, fully-read code weakness (os.Create without O_NOFOLLOW/O_EXCL on the fixed users.backup.json). possibly_valid because the confused-deputy privilege crossing depends on an unresolved deployment assumption — an attacker-writable CWD combined with a more-privileged filebrowser process — which cannot be established from source alone.
Full Evidence
cmd/utils.go:203 fd, err := os.Create(filename) — os.Create = OpenFile(O_RDWR|O_CREATE|O_TRUNC, 0666); no O_NOFOLLOW/O_EXCL and no Lstat pre-check, so an existing symlink at that path is followed and its target truncated
cmd/users_import.go:54-63 the --replace branch calls marshal("users.backup.json", list) with a fixed name in CWD, before deleting the existing user basecmd/users_export.go / cmd/config_export.go pass operator-supplied (non-fixed) paths, so users.backup.json is the uniquely predictable, pre-plantable target
Go standard-library semantics (os package): O_CREATE without O_EXCL/O_NOFOLLOW follows a pre-existing symlink on open+truncate
Proven fact: marshal() writes via os.Create with flags that follow symlinks and truncate the target; there is no O_NOFOLLOW, O_EXCL, or Lstat guard (read directly from cmd/utils.go:202-220).
Proven fact: The backup filename is a hardcoded, predictable users.backup.json created in the process working directory (cmd/users_import.go:60).
Proven fact: If users.backup.json pre-exists as a symlink, the destructive `users import --replace` run truncates and overwrites whatever the symlink points to, with the filebrowser process's privileges — a classic CWE-59 confused-deputy overwrite.
Unvalidated fact: Exploitability requires a deployment/trust assumption not settleable from source: a lower-trust actor must have write access to filebrowser's CWD to pre-plant the symlink, while the victim file is writable by the (more-privileged) filebrowser process but not directly by that actor. In a single-operator CLI run this privilege crossing does not exist.
Unvalidated fact: It also requires the operator to actually run the destructive `users import --replace` command with the attacker-influenced CWD.
cmd/utils.go:203 fd, err := os.Create(filename) — O_RDWR|O_CREATE|O_TRUNC, 0666; no O_NOFOLLOW/O_EXCL and no Lstat pre-check, so an existing symlink is followed and its target truncated
Go os package semantics: O_CREATE without O_EXCL/O_NOFOLLOW follows a pre-existing symlink on open+truncate
Proven fact: marshal() writes via os.Create with flags that follow symlinks and truncate the target; there is no O_NOFOLLOW, O_EXCL, or Lstat guard (cmd/utils.go:202-220).
Proven fact: If users.backup.json pre-exists as a symlink, `users import --replace` truncates and overwrites the symlink's target with the filebrowser process's privileges (CWE-59).
Unvalidated fact: Exploitability requires a deployment/trust assumption not settleable from source: a lower-trust actor must have write access to filebrowser's CWD to pre-plant the symlink, while the victim file is writable by the more-privileged filebrowser process but not directly by that actor. In a single-operator CLI run this privilege crossing does not exist.
Unvalidated fact: It also requires the operator to run the destructive `users import --replace` command with the attacker-influenced CWD.
cmd/utils.go:202-207: marshal() -> os.Create(filename) with no O_NOFOLLOW/O_EXCL and no Lstat pre-check; os.Create = OpenFile(O_RDWR|O_CREATE|O_TRUNC,0666), which follows symlinks and truncates the target
cmd/users_import.go:54-63: `if replace { ... marshal("users.backup.json", list) ... }` — fixed, attacker-predictable filename in the process CWD, written before deleting the user baseContrast cmd/users_export.go:23 and cmd/config_export.go:40 which pass operator-supplied (non-predictable) paths, so users.backup.json is the uniquely predictable symlink target