A stolen FileBrowser account reaches exactly what its Scope points at, and seven of the eight permission toggles stay inside that directory tree. Execute is the exception: combined with one entry in the Commands allowlist, it starts a real process owned by the same system user FileBrowser runs as, and that process ignores Scope, Rules and every other toggle above it. Symlinks and bind-mounts that sit inside Scope are followed like ordinary folders, so the wall is where the filesystem stops, not where the file list stops. Treat Scope, Rules and the toggles as a usability boundary, and treat the container user, its mounts and its capabilities as the security boundary.
TL;DR by reader profile:
- Solo user with one admin account (Tom, one login, photos and documents on a home box): leave Execute off and narrow Scope from the volume root to the two folders you actually open, because a single admin session is the whole application in one credential.
- Household sharer (Ana, one shared media folder for two family members): give each person their own account with Scope pinned to their subfolder and Download plus Share only, since per-user Scope is the one control that reliably keeps two people out of each other's files.
- Small team lead handing out logins (Marek, six contractors working on one asset library): rely on separate accounts with Create, Rename and Modify but never Admin, because the Admin toggle lets any holder rewrite every other account's Scope in seconds.
- Anyone who switched Execute on for one quick script: switch it back off and move the script to a scheduled job outside FileBrowser, as Execute plus a loose Commands entry is a shell, not a file permission.
- NAS or VPS owner who mounted a large parent path (Priya, the whole data disk mapped in for convenience): re-mount only the subtrees you need, several of them read-only, since limits below FileBrowser are the only ones a hijacked session cannot edit.
The tradeoff is blunt: every toggle that makes FileBrowser convenient for a second person widens the area a stolen session can read or rewrite, and the only restrictions that survive that session are the ones enforced underneath the application.
Table of contents
- What can someone do in the first sixty seconds inside a stolen FileBrowser account?
- How does Scope actually work, and what is it anchored to?
- What does each of the eight FileBrowser permission toggles really enforce?
- Why the Execute permission is not a file permission at all
- How does the Commands allowlist decide what is allowed to run?
- What do per-user Rules match against, and in what order are they applied?
- Do symlinks inside Scope walk straight out of it?
- Bind-mounts, volumes and the paths you forgot you handed to FileBrowser
- What does the Admin toggle hand over that the other seven never can?
- How much protection comes from the system user FileBrowser runs as?
- Read-only mounts, dropped capabilities and the limits that live below FileBrowser
- What does the FileBrowser database itself expose if someone reaches it?
- How do you measure your own blast radius with a throwaway account in thirty minutes?
- Does where you run FileBrowser change what a breach can reach?
What can someone do in the first sixty seconds inside a stolen FileBrowser account?
The first minute tells you almost everything, because FileBrowser does not stage its capabilities behind extra confirmations. Whoever holds the session immediately sees the account's Scope as the root of the tree, and every toggle attached to that account is live from the first click. There is no second authentication step for deletion, no confirmation email for a new share link, and no separate re-login before the command console.
Read the entire Scope tree: the file list, the built-in text editor and the preview pane expose every readable file under Scope, including dotfiles such as .env or .ssh/config if they happen to sit inside it.
Pull a bulk copy out: the Download permission zips a selected folder server side and streams it, so a large subtree leaves as one request rather than as hundreds of clicks.
Create a public link: the Share permission mints a link that works for anyone holding the URL, and a link created in the first minute keeps working after you reset the password on the account.
Overwrite or delete in place: Modify and Delete operate directly on the mounted filesystem with no recycle bin and no versioning, so a deleted folder is gone unless your storage layer snapshots it.
Run a process: if Execute is on and the Commands allowlist is not empty, the console runs a real binary as the FileBrowser system user.
The practical consequence is that your recovery plan cannot be "change the password". Revoking existing shares and auditing the tree comes first.
How does Scope actually work, and what is it anchored to?
Scope is not a filter applied to search results. It is the root that FileBrowser hands to the file server for that user, so paths in the URL bar and in the API are resolved against it rather than against the real filesystem root. A user whose Scope is /srv/ana sees /srv/ana/invoices as /invoices, and the layer above is not hidden so much as unreachable by name.
Two roots stack, not one: the server root comes from --root or the "root" key in .filebrowser.json, and each user's Scope is resolved against that root before any request is served.
The Docker image anchors at /srv: the official container sets the root there, which is why a careless -v /:/srv mapping quietly makes the whole host readable to every account in the database.
Scope is stored per user, not per session: it lives in filebrowser.db alongside the account, so changing it with filebrowser users update ana --scope /srv/ana/2024 applies on the next request without a restart.
Path traversal is normalised, not trusted: requests containing ../ are cleaned before the path is joined to Scope, so climbing out with a crafted URL is not the realistic attack here.
Scope is a naming boundary, not an access boundary: anything the operating system user can open through a path inside Scope stays reachable, which is why the two escapes worth worrying about are symlinks and mounts.
Set Scope as narrowly as the person's actual work allows. Widening it later takes one command.
What does each of the eight FileBrowser permission toggles really enforce?
Every toggle maps to a set of API endpoints, not to a mode of thinking. Reading and listing are not toggles at all: any account that can log in can browse its Scope. That baseline matters, because six of the eight only govern writes.
| Toggle | What it gates | What it does not stop |
|---|---|---|
| Admin | User management, global settings, the whole /settings area |
Nothing, it supersedes the other seven |
| Execute | The command console and hook execution | The process itself, which runs outside Scope rules |
| Create | New files, new folders, uploads into Scope | Overwriting an existing file, which Modify covers |
| Rename | Renaming and moving within Scope | Moving a file into a share, which stays readable |
| Modify | Writing to existing files, including the text editor | Silent truncation, there is no version history |
| Delete | Removing files and folders | Recovery, since there is no recycle bin |
| Share | Creating public links to anything under Scope | Links already issued, which outlive the toggle |
| Download | Single files and server-side zipped folders | Reading content through preview and the editor |
Two consequences are easy to miss. Turning Download off does not create a read-only viewer, because the built-in editor still renders text files in the browser. Turning Share off later does not retire links created earlier, so revocation is a separate step in the shares list.
A practical baseline for a second person is Download plus Share off, Create and Modify on, Delete off, Execute off, Admin off. Set it with filebrowser users update <name> --perm.delete=false --perm.execute=false and verify it from a logged-out browser.
Why the Execute permission is not a file permission at all
The other seven toggles all end in a filesystem call that FileBrowser makes on your behalf, inside Scope. Execute ends in a process. Once a binary starts, FileBrowser is no longer in the path between that process and the disk, and none of its checks apply to what the process does next.
The process inherits the system user, not the account: it runs as whatever UID the FileBrowser binary runs as, so it can read and write every path that UID can reach, including paths far above Scope.
Scope becomes a working directory, not a boundary: commands start in the folder the user is standing in, but an absolute path such as /etc or /srv resolves normally from inside the process.
A configured shell turns one allowed word into arbitrary syntax: if you have run filebrowser config set --shell "/bin/sh -c", an allowlisted command is passed to a shell that also accepts pipes, semicolons and redirection.
Hooks execute without anyone clicking a console: the global Commands settings attach to events such as before_save and after_upload, so an uploaded file can trigger a command run without an interactive session.
Rules and Scope are never consulted: the allow and deny patterns are enforced by the file API, and a spawned process does not go through the file API.
Treat Execute as equivalent to shell access for that user. If one account needs a periodic script, run it from cron or a systemd timer outside the container and leave perm.execute false for every account in filebrowser.db.
How does the Commands allowlist decide what is allowed to run?
The allowlist is a per user list of words, and the check is a prefix match on the first token of what the user types. Allowing git allows every subcommand and every flag that git accepts. Allowing sh allows everything. The list answers "which binary", never "with which arguments".
Matching stops at the first word: filebrowser users update ana --commands "git,rsync" permits git clone, git config and rsync to any reachable path, because nothing after the first token is inspected.
Binaries still have to exist in the image: the official FileBrowser container is Alpine based and ships busybox, so ls, cp, wget and sh are present even though nobody installed them deliberately.
A busybox wget is an exfiltration path: one allowlisted networking tool turns read access into outbound transfer, which matters more than write access for a privacy focused setup.
An empty list is the real off switch: leaving Commands empty while Execute stays on gives no usable console, but the safer pair is Execute off and the list empty.
Where you run the app decides how much a permitted command finds around it. A self managed VPS carries a full distribution with package managers and SSH keys present on disk. A NAS adds the vendor's own tooling. Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user, so FileBrowser sits in a container image rather than on a general purpose host, as do the equivalent setups on a home server or a VPS running Docker.
Review the list per account, not once globally.
What do per-user Rules match against, and in what order are they applied?
Rules are the only control that works below the folder level. Everything else grants or denies a whole Scope. A rule is four things: allow or deny, a path or a regular expression, the pattern itself, and the user it belongs to. Default behaviour is allow, so rules exist to carve exceptions out of an otherwise visible tree.
Patterns are matched relative to Scope: a rule written for /secrets matches what the user sees as /secrets, not the host path, so moving Scope changes which files a rule covers.
Regular expressions use Go's RE2 syntax: filebrowser rules add --user ana --regex "\.env$" works, while lookahead and backreferences are not supported and will be rejected rather than silently ignored.
Two lists are in play: there is a global rule list in settings and a per-user list on the account, and a file that survives one list can still be caught by the other, so test the combination rather than reasoning about precedence on paper.
Denied paths disappear from listings and from search: the entry is filtered out of the directory response, which is what makes rules feel like hiding rather than permission.
A deny rule is checked on the request path only: a file reachable under a second name, for example through a folder that a pattern does not cover, is served normally.
Verify with a direct request, not the interface. Log in as the restricted account, then fetch the hidden path by URL and confirm you get an error rather than content. Three well chosen deny rules on dotfiles, key material and backup archives cover most of what a stolen session would want.
Do symlinks inside Scope walk straight out of it?
Yes. FileBrowser cleans ../ out of request paths before joining them to Scope, but it does not resolve symlinks before deciding whether a path is inside Scope. A link that already exists on disk is opened by the operating system like any other directory entry, and the content on the far side is served normally.
The link only has to sit inside Scope: if /srv/ana/host points at /etc, the account browsing /host reads files two levels above its own root, and nothing in the interface signals that it left.
Users cannot create one through the web interface: there is no symlink endpoint in the file API, so the links that matter are ones placed by you, by a backup tool, or by another application sharing the same volume.
Execute closes that gap instantly: a single allowlisted command running ln -s / /srv/ana/root converts a read of one folder into a read of everything the process user can open.
Uploads can carry links inside archives: if an extraction step outside FileBrowser unpacks a tar that contains symlinks, the links land in Scope without anyone typing a command.
Deny rules do not help here: a rule written for /etc matches the path as the user sees it, so the same target reached through /host is not covered.
Audit it directly. Run find /srv -type l -ls against the mounted tree and confirm every result points somewhere you intended. Run it again after restoring a backup, because archive restores are the common way an old link reappears in a Scope that has since been narrowed.
Bind-mounts, volumes and the paths you forgot you handed to FileBrowser
Scope decides what an account sees. The mount list decides what exists to be seen at all. A narrow Scope on top of a wide mount is one admin edit away from being a wide Scope, because an account with the Admin toggle can point Scope at anything the container has.
| Mount line | What any account can potentially reach | What it buys you |
|---|---|---|
-v /:/srv |
Every file the container user can open on the host | Nothing you cannot get another way |
-v /home/tom:/srv |
Shell history, .ssh, application tokens under .config
|
One less path to type |
-v /srv/media:/srv/media |
Only the media tree, matching the path on both sides | Predictable paths in backups and logs |
-v /srv/archive:/srv/archive:ro |
The archive tree, readable but not writable | Delete and Modify stop at the kernel |
| Named volume | Only what you copy into it | Isolation from the host tree |
Mount the narrowest set of subtrees that the people using FileBrowser actually need, and mount anything historical read-only. Two or three separate mounts are easier to reason about than one parent path.
Check what is really attached rather than what the compose file says, because an edited file that was never re-deployed proves nothing. Run docker inspect -f '{{json .Mounts}}' filebrowser and read the list. The same applies on a NAS package or a Yundera app install, where the mapping is set once during installation and then forgotten.
Anything on that list is inside the blast radius, whether or not a Scope currently points at it.
What does the Admin toggle hand over that the other seven never can?
Admin is not a stronger version of Modify. It moves the account from working on files to editing the rules about files, which means it can grant itself anything the other seven withhold. That is why an admin session is not bounded by its own Scope in any meaningful sense.
It rewrites Scope, including its own: an admin opens Users, sets Scope to the server root, and every mounted path becomes browsable in one save with no restart.
It flips permissions on any account: Execute and the Commands list are editable from the same form, so the path from Admin to a running process is two clicks rather than a separate vulnerability.
It creates accounts that outlive your response: a new user added quietly keeps working after you reset the password on the account that was originally stolen.
It edits global settings, not just users: branding, the shell used for commands and the global rule list all live behind the Admin toggle, and global rules govern accounts you never touched.
It reads the shares list for everyone: existing public links become visible and reusable, including links created by other people.
Keep exactly one admin account, use it only for administration, and give yourself a second non admin account for daily file work. The blast radius of a compromised daily driver then stops at one Scope. If two people genuinely need to add users, accept that you have two accounts that can reach every mount, and set --perm.admin=false on everyone else with filebrowser users update <name> --perm.admin=false. Review the user list monthly and confirm the count of admins is still one.
How much protection comes from the system user FileBrowser runs as?
More than any toggle in the interface, because this is the limit a hijacked session cannot edit. FileBrowser can only open what its UID and GID can open, and that check happens in the kernel rather than in the application.
Running as root erases the filesystem layer entirely: with a bind mount, root inside the container is root on the host files, so ownership and mode bits stop filtering anything and a read-only intention becomes a suggestion.
A dedicated UID turns ownership into the real allowlist: set user: "1000:1000" in your compose file, then FileBrowser reaches media owned by that UID and gets permission denied on everything else, including files reached through a symlink.
World readable files defeat ownership on their own: a file at mode 0644 is readable by every UID on the system, so tighten the sensitive ones to 0640 or 0600 rather than relying on who owns them.
The database and config still need write access: the chosen UID must own the directory holding filebrowser.db, otherwise the container starts and then fails on the first settings change.
Verify rather than assume. Run docker exec filebrowser id and read the numbers, then stat -c '%U %G %a' /srv/media on the host and compare.
The hosting shape decides how much of this you set yourself. Yundera is a managed Personal Cloud Server built on CasaOS, where self-hosted apps run as Docker containers on a server dedicated to the user and that data stays on the user's own server. A VPS, a home server or a NAS puts the same UID decision in your hands.
Read-only mounts, dropped capabilities and the limits that live below FileBrowser
These controls sit under the application, so no permission edit and no admin session can switch them off. They are also the only ones that constrain a process started through Execute, which every control inside FileBrowser fails to reach.
| Control | What it actually stops | What it costs you |
|---|---|---|
read_only: true on the container |
Any write outside declared volumes, including a dropped binary in /tmp
|
You must add a tmpfs for scratch space and keep the database on its own volume |
cap_drop: - ALL |
Capability based tricks such as changing file ownership with chown
|
Nothing for normal browsing, uploading and downloading |
security_opt: - no-new-privileges:true |
A setuid binary inside the image raising privileges mid process | Nothing, FileBrowser does not need it |
pids_limit and a memory limit |
A runaway or deliberate fork loop taking the host down with it | A hard ceiling you have to size once |
| An internal Docker network with no egress | Outbound copies of your files to somewhere else | Breaks any workflow that expects the container to fetch from the internet |
Apply them in that order. Read-only plus dropped capabilities is roughly fifteen minutes of work and survives every future permission mistake you make in the interface.
Test each one after applying it rather than trusting the compose file. Run docker exec filebrowser touch /test and confirm it fails, then confirm that uploading a file through the web interface still succeeds. A read-only container that also blocks legitimate uploads means a volume is missing, not that the control is wrong.
What does the FileBrowser database itself expose if someone reaches it?
Everything FileBrowser knows lives in one bbolt file, normally filebrowser.db. It is not encrypted at rest. If that file ends up inside a Scope, or inside a mount that Scope can reach, a download turns an account compromise into full control of the installation.
It holds the token signing key: the settings bucket stores the key used to sign session tokens, and anyone holding it can mint a valid token for any account without ever knowing a password.
Password hashes are bcrypt, so they resist cracking but not replacement: an attacker with a writable copy does not need to crack anything, they set a new hash and log in as that user.
Every share record travels with it: share hashes and their password hashes are in the same file, so a copy exposes links you issued months ago.
The whole user table is offline readable: filebrowser -d /database/filebrowser.db users ls prints usernames, Scope paths and permission flags from any copy of the file, on any machine.
Backups inherit the same exposure: a nightly archive of the config directory dropped into a browsable folder is the same leak with extra steps.
Store the database outside every served path. A separate mount such as /database for the file and /srv for content is the simple layout, and it keeps the two concerns apart even if you later widen a Scope. Then confirm it from the inside: log in as a restricted account and search for filebrowser.db. Zero results is the outcome you want, and it takes about a minute to check.
How do you measure your own blast radius with a throwaway account in thirty minutes?
Reasoning about permissions on paper produces the wrong answer often enough that it is worth testing. The test is cheap, repeatable, and the only honest way to know what a stolen session sees.
Create the account, 2 minutes: run filebrowser users add audit <password> --perm.admin=false with the same toggles and Scope as your least trusted real user.
Walk upward in a private window, 5 minutes: log in, click to the top of the tree, and write down the highest folder you can list. That folder is your ceiling, not the one in your notes.
Probe the API directly, 10 minutes: the interface hides things the API will still serve. Get a token with curl -X POST -d '{"username":"audit","password":"..."}' https://files.example.com/api/login, then request a path you believe is denied with curl -H "X-Auth: <token>" https://files.example.com/api/resources/secrets. A 403 or 404 is a pass, a JSON listing is a finding.
Test the toggles you believe are off, 5 minutes: try to delete a scratch file, try to create a share, and open the command console. Each failure should be a refusal, not an empty screen you interpret as a refusal.
Check a share from a logged out browser, 3 minutes: copy any link the account can produce and open it in a browser with no session.
Remove the account and its shares, 2 minutes: delete the user, then confirm the shares it created are gone from the list rather than assuming they left with it.
Repeat after any mount or Scope change.
Does where you run FileBrowser change what a breach can reach?
Yes, because the blast radius is defined by what else lives on the machine. The same account with the same Scope reaches very different things depending on what is sitting one directory above it.
Advantages of hosting it yourself:
- You choose the neighbours: a box that holds only media files has nothing else to lose, while a general purpose server holds SSH keys, other applications and their databases in the same filesystem.
- You set the mount list: nobody else decides which subtrees are attached, so the ceiling from your throwaway account test is a decision rather than a default.
- The files never sit in a shared multi-tenant store: a compromise of your account is a compromise of your server, not an entry point into a pooled service.
- Retention is yours: snapshots, backup frequency and how long a deleted file really survives are settings you own.
Yundera is a managed Personal Cloud Server, built on CasaOS, that runs self-hosted apps as Docker containers on a server dedicated to the user. A self managed VPS, a home server and a NAS put the same FileBrowser container on hardware you control in different ways.
Checklist before you expose it:
- One purpose per host: no SSH keys, no unrelated application data under any mounted path.
- HTTPS only: FileBrowser sends credentials in a login request, so plain HTTP on a shared network is not acceptable.
- Mount list reviewed: every entry justified out loud.
- Database outside every served path: verified by search, not by memory.
- Snapshots enabled: because Delete is permanent at the application layer.
Top comments (0)