Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
b7dc392
docs: fix typo
hacdias Jul 21, 2026
c05c668
fix: process --FollowExternalSymlinks
hacdias Jul 25, 2026
7361d91
fix(upload): handle encoded path conflicts safely (#6040)
archit-goyal Jul 25, 2026
b21b124
fix: accessibility and security improvements (#6033)
superafun Jul 25, 2026
6232686
chore: update translations
transifex-integration[bot] Jul 25, 2026
5a12cad
docs: update readme
hacdias Jul 25, 2026
9fffee3
docs: remove go report
hacdias Jul 25, 2026
8ddd3d1
fix(auth): isolate auto-provisioned proxy and hook users to their own…
hacdias Jul 25, 2026
4b8a8d7
fix(storage): reject case-folded home directory collisions
hacdias Jul 25, 2026
6c69b5c
fix(http): enforce download permission on the checksum branch
hacdias Jul 25, 2026
9bd79c3
fix(http): delete abandoned TUS uploads through the scoped filesystem
hacdias Jul 25, 2026
4daddec
fix(http): enforce declared Upload-Length on TUS uploads
hacdias Jul 25, 2026
cd5749d
test(auth): cover per-user scope isolation for proxy and hook provisi…
hacdias Jul 25, 2026
7453c78
test(storage): cover case-insensitive GetByScope matching
hacdias Jul 25, 2026
610e0b0
test(http): cover download-permission gate on the checksum branch
hacdias Jul 25, 2026
3213b60
test(http): cover scope-safe removal on TUS upload eviction
hacdias Jul 25, 2026
4c3cb4b
test(http): cover TUS Upload-Length enforcement
hacdias Jul 25, 2026
328f629
chore(release): 2.63.19
hacdias Jul 25, 2026
392253c
docs: update SECURITY.md and add CLAUDE.md
hacdias Jul 25, 2026
660d846
docs: update CLAUDE.md
hacdias Jul 25, 2026
4a4b0f8
docs: update CLAUDE instructions
hacdias Jul 25, 2026
67e893e
fix: use aria-selected
hacdias Jul 25, 2026
fb6aeba
fix(users): make the provisioned scope check atomic with the save
hacdias Jul 25, 2026
bc3d75a
test(users): cover concurrent provisioning into one scope
hacdias Jul 25, 2026
50125a5
chore(release): 2.63.20
hacdias Jul 25, 2026
032271b
chore(deps): update all non-major dependencies (#5946)
renovate[bot] Jul 26, 2026
0dd8905
Revert "chore(deps): update all non-major dependencies (#5946)"
hacdias Jul 26, 2026
41b01a7
chore: update Go deps
hacdias Jul 26, 2026
41e2b1b
docs: update CLAUDE.md
hacdias Jul 26, 2026
e6d70cf
fix(http): canonicalize paths before checking access rules (#6045)
hacdias Jul 26, 2026
f6fb8f6
chore(release): 2.63.21
hacdias Jul 26, 2026
ac11538
chore: update renovate.json
hacdias Jul 26, 2026
dab6415
merge upstream/master into oadp-dev
oadp-maintainers Jul 27, 2026
7c03236
UPSTREAM: <carry> Add disableUserProfile branding option
mpryc Oct 18, 2025
161a1bb
UPSTREAM: <carry> Add defaultLoginUser branding option
mpryc Oct 18, 2025
c75921e
UPSTREAM: <carry> Disable delete and rename from Help
mpryc Oct 19, 2025
a621988
UPSTREAM: <carry> Containerfile to be used with UBI
mpryc Nov 12, 2025
e345076
UPSTREAM: <carry> Add OWNERS file
mpryc Feb 5, 2026
9a69c49
UPSTREAM: <carry> Fix broken rebase of branding options
mpryc Feb 9, 2026
1e92749
UPSTREAM: <carry>: fix github actions
mpryc Apr 14, 2026
e05c02e
Add dependabot config grouping all Go module dependency updates
Copilot Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# CLAUDE.md

Guidance for Claude when working in this repository (File Browser, `filebrowser/filebrowser`).

## Repo orientation

- Go backend (`github.com/filebrowser/filebrowser/v2`, ecosystem `go`) + Vue frontend under `frontend/`.
- The project is in **maintenance-only mode** (see `SECURITY.md`). Prefer small, surgical, well-tested changes.
- Version scheme: `v2.63.x`. Conventional-commit messages (`fix(scope): …`, `feat: …`, `chore: …`).
- Verify with `go build ./...`, `go vet ./...`, `go test ./...`. Reuse existing test harnesses (e.g. `signToken`, `scopedUserStorage`, `handle`, `customFSUser`, `mockUserStore`).

---

# Handling security advisories

Use this playbook when asked to triage, verify, fix, or manage GitHub security advisories.
All advisory state lives on GitHub and is driven with the `gh` CLI. States are
`triage → draft → published`, plus `closed`.

## 1. Fetch

```bash
# List by state (also: published, draft, closed)
gh api '/repos/filebrowser/filebrowser/security-advisories?state=triage&per_page=100' \
--jq '.[] | {ghsa_id, severity, summary, state}'

# Full report for one advisory (read .summary and .description)
gh api /repos/filebrowser/filebrowser/security-advisories/GHSA-xxxx-xxxx-xxxx \
--jq '.summary, "---", .description'
```

Always pull the **published** set too — you need it to dedup against.

## 2. Verify each report — do NOT trust the report text

Read the actual source **at HEAD** and reach one verdict per advisory:

- **CONFIRMED** — defect exists at HEAD. Quote the exact `file:line`.
- **FIXED** — already patched (find the fix commit).
- **FALSE / NOT APPLICABLE** — claim is wrong, or targets a different project.
- **NOT EXPLOITABLE** — pattern exists but no code path can reach the precondition.
- **DUPLICATE** — of a published advisory, or of another triage advisory.

Common traps — check each before accepting a report:

- **"Incomplete fix of a prior advisory."** Read the original fix commit and confirm the *specific*
sibling code path is actually still unguarded — a prior fix may already cover a related path.
- **Wrong project.** Confirm the referenced files, symbols, and endpoints exist in this repo; reports
sometimes describe a fork. If the cited code isn't here, close as not applicable.
- **"Legacy / upgraded / imported records are affected."** Trace the field's git history and confirm that
some released version could actually produce such a record before believing it
(`git log -S'Field' -- path`, `git show <commit>`, `git tag --contains <commit>`).
- **Overlapping reports.** Two triage advisories may share one root cause — consolidate and fix once.
- **Known, intentionally-unaddressed classes.** Some areas are known and tracked but not fixed (see
`SECURITY.md`'s Known Issues); matching reports are duplicates.

Record, per advisory: verdict, the `file:line` evidence, exploitability preconditions
(default config? platform-specific? auth required?), and the disposition.

## 3. Fix (one commit per advisory)

- Branch off `master` first; never commit fixes directly to `master`.
- **One commit per fix**, each referencing the GHSA in the body (`Refs GHSA-xxxx-xxxx-xxxx`).
- When several advisories share a root cause because parallel code paths diverged, **centralize** the logic
(e.g. `settings.CreateUserHome` used by signup, proxy, and hook provisioning) so they cannot drift again.
- Keep it surgical; match surrounding style.

## 4. Add a regression test per fix

- Reuse the existing harnesses; add a focused test asserting the fixed behavior (and that the legitimate
path still works). Commit tests alongside the fixes (`test(scope): …`, `Refs GHSA-…`).
- Run `go test ./... && go vet ./...` before finishing.

## 5. Severity — set a CVSS vector, don't hand-assert

Set a CVSS v3.1 vector; GitHub derives the score **and** severity from it (overriding the plain `severity`
field). Encode the real preconditions in the vector so the band is defensible:

- Requires a specific target configuration/platform (e.g. case-insensitive FS) → **AC:H**.
- Unauthenticated vs needs an account → **PR:N / PR:L**.
- Keep it consistent with the **predecessor CVE's** rating (an incomplete-fix follow-up should not outrank
its parent). Bands: `0.1–3.9` low, `4.0–6.9` medium, `7.0–8.9` high, `9.0–10.0` critical.

```bash
gh api -X PATCH /repos/filebrowser/filebrowser/security-advisories/GHSA-xxxx-xxxx-xxxx \
-f cvss_vector_string='CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H' \
--jq '{ghsa_id, severity, score: .cvss.score, vector: .cvss.vector_string}'
```

## 6. Clean up the title

Rewrite reporter titles to the concise, sentence-case, backtick-free style of the published advisories
(state the vuln; drop "Incomplete fix of…" prefixes and jargon). The title is the `summary` field:

```bash
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx \
-f summary='Proxy-auth auto-provisioning ignores createUserDir and grants the server root scope'
```

## 7. Rewrite the body into the standard structure

Reporter reports arrive in whatever shape the reporter used. Before drafting, rewrite the
`description` into the sections below — **reusing the reporter's own wording wherever it is
accurate**, rather than paraphrasing it. Drop the greeting, the offer to help, and any claim the
verification in step 2 disproved. Keep sections in this order and omit the ones that don't apply:

| Section | Contents |
| --- | --- |
| `## Summary` | What the defect is, in two or three sentences. Note the version it was reported against and the range it was verified over. |
| `## Details` | Root cause, naming `file.go`, the function, and a short quote of the **pre-fix** code. |
| `## PoC` | The reporter's reproduction steps and observed result, trimmed to the essentials. |
| `## Impact` | Who can exploit it (privilege level, preconditions) and what they get. |
| `## Patches` | Fixed version plus a link to the fix commit, and one sentence on what the fix does. Mention it if the reporter re-tested and confirmed. |
| `## Workarounds` | Real mitigations, or `None. Upgrade to vX.Y.Z.` |
| `## Out of scope` | Anything in the original report deliberately **not** treated as a vulnerability, with the reasoning. Needed whenever the advisory is narrower than the report. |
| `## References` | Fix and regression-test commit links. |

Use `##` headings (matching the published advisories) and keep the body in the maintainer's voice —
first person ("I reproduced…") belongs only inside quoted PoC steps.

Send it as a file so the Markdown survives shell quoting:

```bash
jq -Rs '{description: .}' desc.md \
| gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx --input -
```

## 8. Set affected & patched versions

The package is always `{ecosystem: "go", name: "github.com/filebrowser/filebrowser/v2"}`.

- `vulnerable_version_range``<= <latest released version>` (the newest tag; find it with
`git tag --list 'v2.*' --sort=-version:refname | head -1`).
- `patched_versions` — the **next** release that will actually ship the fix. It doesn't exist just
because the branch does; it still has to be cut. **When unsure which version that will be, ask.**

Replace the placeholder versions below before sending:

```bash
printf '%s' '{"vulnerabilities":[{"package":{"ecosystem":"go","name":"github.com/filebrowser/filebrowser/v2"},"vulnerable_version_range":"<= <latest>","patched_versions":"<next>","vulnerable_functions":[]}]}' \
| gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx --input -
```

## 9. Move state / close, by disposition

```bash
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx -f state=draft # fixed, awaiting release
gh api -X PATCH .../security-advisories/GHSA-xxxx-xxxx-xxxx -f state=closed # duplicate / N-A / not-exploitable
```

- **CONFIRMED & fixed** → metadata done → **draft**. Publish after the patched release ships.
- **DUPLICATE / NOT APPLICABLE / NOT EXPLOITABLE****closed**.
- **DEFERRED** (confirmed but not yet fixed) → leave in **triage** with a note.
- The REST API **cannot post advisory comments** — replies to reporters (dup notice, evidence, links to
the relevant tracking issue) must be posted manually in the advisory UI. Draft the text for the maintainer.

## 10. Release & publish

Push the branch, open a PR, merge, tag/release the `patched_versions` you set, then publish the drafts.
Confirm outward-facing/irreversible advisory actions (close, publish) with the maintainer before doing them.
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.

## [2.63.21](https://github.com/filebrowser/filebrowser/compare/v2.63.20...v2.63.21) (2026-07-26)

### Bug Fixes

* **http:** canonicalize paths before checking access rules ([#6045](https://github.com/filebrowser/filebrowser/issues/6045)) ([e6d70cf](https://github.com/filebrowser/filebrowser/commit/e6d70cf24c0cd79a1787601dc99104ec7e7ca3ef))

### Reverts

* Revert "chore(deps): update all non-major dependencies (#5946)" ([0dd8905](https://github.com/filebrowser/filebrowser/commit/0dd89058867f87a7bc04aa7517d21528a280e27c)), references [#5946](https://github.com/filebrowser/filebrowser/issues/5946)
## [2.63.20](https://github.com/filebrowser/filebrowser/compare/v2.63.19...v2.63.20) (2026-07-25)

### Bug Fixes

* use aria-selected ([67e893e](https://github.com/filebrowser/filebrowser/commit/67e893eee7ee411e166d3fcd759a87b6f0971277))
* **users:** make the provisioned scope check atomic with the save ([fb6aeba](https://github.com/filebrowser/filebrowser/commit/fb6aeba9eae7b8eb401e0db325973781e1ffd08b))
## [2.63.19](https://github.com/filebrowser/filebrowser/compare/v2.63.18...v2.63.19) (2026-07-25)

### Bug Fixes

* accessibility and security improvements ([#6033](https://github.com/filebrowser/filebrowser/issues/6033)) ([b21b124](https://github.com/filebrowser/filebrowser/commit/b21b1245ae1b57f031b2f5d787f32a17532402c0))
* **auth:** isolate auto-provisioned proxy and hook users to their own home ([8ddd3d1](https://github.com/filebrowser/filebrowser/commit/8ddd3d1db9b9f5727d0bf96ea0e7d9a25a8692b4))
* **http:** delete abandoned TUS uploads through the scoped filesystem ([9bd79c3](https://github.com/filebrowser/filebrowser/commit/9bd79c3aaeb4a55b0e69cf8976c4a258db2f5e06))
* **http:** enforce declared Upload-Length on TUS uploads ([4daddec](https://github.com/filebrowser/filebrowser/commit/4daddec6f200b03a721197d8c0b4b652c994894e))
* **http:** enforce download permission on the checksum branch ([6c69b5c](https://github.com/filebrowser/filebrowser/commit/6c69b5cd895e15b29e563200a9a6dfc27b06525e))
* **http:** run upload hooks for directories ([#6034](https://github.com/filebrowser/filebrowser/issues/6034)) ([9b78324](https://github.com/filebrowser/filebrowser/commit/9b78324d773c790951cc6a97840c4b55f66b5f3d))
* process --FollowExternalSymlinks ([c05c668](https://github.com/filebrowser/filebrowser/commit/c05c66814891c3cccef394162e428444b53394e4))
* return error instead of panicking on an unreadable directory during copy ([#6020](https://github.com/filebrowser/filebrowser/issues/6020)) ([ac46cf0](https://github.com/filebrowser/filebrowser/commit/ac46cf06719575477d5125e7472037c204b3702d))
* **storage:** reject case-folded home directory collisions ([4b8a8d7](https://github.com/filebrowser/filebrowser/commit/4b8a8d72ce554dde378b5091da74aa930ea18327))
* **upload:** handle encoded path conflicts safely ([#6040](https://github.com/filebrowser/filebrowser/issues/6040)) ([7361d91](https://github.com/filebrowser/filebrowser/commit/7361d91ea2e8cc200a0f40ac84adcd02aedc9ed2))
## [2.63.18](https://github.com/filebrowser/filebrowser/compare/v2.63.17...v2.63.18) (2026-07-04)


Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
</p>

[![Build](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml/badge.svg)](https://github.com/filebrowser/filebrowser/actions/workflows/ci.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/filebrowser/filebrowser/v2)](https://goreportcard.com/report/github.com/filebrowser/filebrowser/v2)
[![Version](https://img.shields.io/github/release/filebrowser/filebrowser.svg)](https://github.com/filebrowser/filebrowser/releases/latest)

File Browser provides a file managing interface within a specified directory and it can be used to upload, delete, preview and edit your files. It is a **create-your-own-cloud**-kind of software where you can just install it on your server, direct it to a path and access your files through a nice web interface.
Expand All @@ -19,7 +18,7 @@ This project is a finished product which fulfills its goal: be a single binary w
- It can take a while until someone gets back to you. Please be patient.
- [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions).
- The priority is triaging issues, addressing security issues and reviewing pull requests meant to solve bugs.
- No new features are planned. Pull requests for new features are not guaranteed to be reviewed.
- No new features are planned. Pull requests for new features will not be reviewed.

Please read [@hacdias' personal reflection](https://hacdias.com/2026/03/11/filebrowser/) on the project status.

Expand Down
37 changes: 22 additions & 15 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,32 @@

## Supported Versions

Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | --------- |
| 2.x | ✅ |
| < 2.0 | ❌ |

| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
## Before Reporting

## Reporting a Vulnerability
This project is in maintenance-only mode. To avoid duplicates, first check the [existing advisories](https://github.com/filebrowser/filebrowser/security/advisories) and open issues, and confirm:

- **It concerns this project, not a fork.** Reports about code, features, or endpoints that don't exist here belong to the relevant fork.
- **It isn't an already-known class** that remains unaddressed:
- Command execution, runner, and hooks (opt-in, disabled by default) — [#5199](https://github.com/filebrowser/filebrowser/issues/5199)
- Session and JWT handling — [#5216](https://github.com/filebrowser/filebrowser/issues/5216)

Vulnerabilities with critical impact should be reported on the [Security](https://github.com/filebrowser/filebrowser/security) page of this repository, which is a private way of communicating vulnerabilities to maintainers. This project is in maintenance-only mode and it can take a while until someone gets back to you.
Reports covering these are likely to be closed as duplicates.

## Reporting a Vulnerability

If it is not a critical vulnerability, please open an issue and we will categorize it as a security issue. By giving visibility, we can get more help from the community at fixing such issues.
- **Critical:** report privately via the [Security](https://github.com/filebrowser/filebrowser/security) page.
- **Non-critical:** open a public issue so the community can help; we'll label it as a security issue.
Comment on lines +23 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Keep suspected vulnerability reports private until triage.

Directing “non-critical” vulnerabilities to public issues can disclose exploitable details before the project confirms impact or ships a fix, and “critical” is not defined here. Route all suspected vulnerabilities through the private Security channel, or document precise criteria for cases safe to disclose publicly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SECURITY.md` around lines 23 - 24, Update the SECURITY.md
vulnerability-reporting guidance to route all suspected vulnerabilities through
the private Security page during triage. Remove the severity-based public issue
instruction, or replace it with precise criteria explicitly defining when public
disclosure is safe.


When reporting an issue, where possible, please provide at least:
Please include, where possible:

* The commit version the issue was identified at
* A proof of concept (plaintext; no binaries)
* Steps to reproduce
* Your recommended remediation(s), if any.
- The commit the issue was found at
- A plaintext proof of concept (no binaries)
- Steps to reproduce
- Recommended remediation, if any

The File Browser team is a volunteer-only effort, and may reach back out for clarification.
We're a volunteer effort, so responses can take a while, and we may reach out for clarification.
13 changes: 7 additions & 6 deletions auth/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,15 +157,16 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
}
u = a.GetUser(d)

userHome, err := a.Settings.MakeUserDir(u.Username, u.Scope, a.Server.Root)
// A scope explicitly returned by the hook takes precedence over the
// automatic per-user home directory derivation.
_, explicitScope := a.Fields.Values["user.scope"]
derivedScope, err := a.Settings.CreateUserHome(u, a.Server.Root, explicitScope)
if err != nil {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
return nil, err
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
log.Printf("user: %s, home dir: [%s].", u.Username, u.Scope)

err = a.Users.Save(u)
if err != nil {
if err := a.Users.SaveProvisioned(u, derivedScope); err != nil {
Comment on lines +160 to +169

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files matching hook.go:\n'
fd -a 'hook\.go$' . || true

printf '\nRelevant auth/hook.go excerpt:\n'
sed -n '1,240p' auth/hook.go

printf '\nSearch CreateUserHome/SaveProvisioned/MakeUserDir definitions:\n'
rg -n "func .*CreateUserHome|func .*SaveProvisioned|func .*MakeUserDir|type .*Fields|GetString\\(" -S .

Repository: migtools/filebrowser

Length of output: 12609


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'auth/hook.go hookFields implementation:\n'
sed -n '230,310p' auth/hook.go

printf '\nsettings/dir.go CreateUserHome/MakeUserDir implementation:\n'
sed -n '1,120p' settings/dir.go

printf '\nusers/storage.go SaveProvisioned implementation:\n'
sed -n '80,170p' users/storage.go

printf '\nsettings dir tests around CreateUserHome:\n'
sed -n '1,120p' settings/dir_test.go

printf '\nfield string parsing probe for present/empty user.scope:\n'
python3 - <<'PY'
def getValues(s):
    m={}
    for line in s.replace('\r\n','\n').splitlines():
        v=line.split('=',1)
        if len(v)==2:
            k=fv=v[0].strip(), v[1].strip()
            m[k]=fv
    return m

for s in ['user.scope=\nhook.action=auth\n','user.scope=/home/alice\nhook.action=auth\n','hook.action=auth\n']:
    print(repr(s), getValues(s), "has user.scope:", "user.scope" in getValues(s).keys(), "value:", repr(getValues(s).get("user.scope","<missing>")))
PY

Repository: migtools/filebrowser

Length of output: 8538


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'auth/hook.go hookFields implementation:\n'
sed -n '230,310p' auth/hook.go

printf '\nsettings/dir.go CreateUserHome/MakeUserDir implementation:\n'
sed -n '1,120p' settings/dir.go

printf '\nusers/storage.goSaveProvisioned implementation:\n'
sed -n '80,170p' users/storage.go

printf '\nsettings dir tests around CreateUserHome:\n'
sed -n '1,120p' settings/dir_test.go

printf '\nfield string parsing probe for present/empty usser.scope:\n'
python3 - <<'PY'
def getValues(s):
    m={}
    for line in s.replace('\r\n','\n').splitlines():
        v=line.split('=',1)
        if len(v)==2:
            m[v[0].strip()]=v[1].strip()
    return m

for s in ['user.scope=\nhook.action=auth\n','user.scope=/home/alice\nhook.action=auth\n','hook.action=auth\n']:
    print(repr(s), getValues(s), "has user.scope:", "user.scope" in getValues(s), "value:", repr(getValues(s).get("user.scope", "<missing>")))
PY

Repository: migtools/filebrowser

Length of output: 8393


Require a non-empty value for hook-provided explicit scopes.

user.scope= is parsed as present with value ""; GetUser then uses that empty value, while explicitScope := true keeps CreateUserHome from reporting a derived scope. With CreateUserDir enabled, MakeUserDir still auto-derives a per-username home, but SaveProvisioned(u, false) skips the concurrency-safe scope collision check. Treat the scope as explicit only when the hook supplied a non-empty value.

🔒 Proposed fix
-		// A scope explicitly returned by the hook takes precedence over the
-		// automatic per-user home directory derivation.
-		_, explicitScope := a.Fields.Values["user.scope"]
+		// A scope explicitly returned by the hook takes precedence over the
+		//automatic per-user home directory derivation. Treat the scope as
+		// explicit only when the hook actually supplied a non-empty value —
+		// an empty value must still go through automatic per-user derivation
+		// and its collision check.
+		hookScope, hasHookScope := a.Fields.Values["user.scope"]
+		explicitScope := hasHookScope && hookScope != ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// A scope explicitly returned by the hook takes precedence over the
// automatic per-user home directory derivation.
_, explicitScope := a.Fields.Values["user.scope"]
derivedScope, err := a.Settings.CreateUserHome(u, a.Server.Root, explicitScope)
if err != nil {
return nil, fmt.Errorf("user: failed to mkdir user home dir: [%s]", userHome)
return nil, err
}
u.Scope = userHome
log.Printf("user: %s, home dir: [%s].", u.Username, userHome)
log.Printf("user: %s, home dir: [%s].", u.Username, u.Scope)
err = a.Users.Save(u)
if err != nil {
if err := a.Users.SaveProvisioned(u, derivedScope); err != nil {
// A scope explicitly returned by the hook takes precedence over the
// automatic per-user home directory derivation. Treat the scope as
// explicit only when the hook actually supplied a non-empty value —
// an empty value must still go through automatic per-user derivation
// and its collision check.
hookScope, hasHookScope := a.Fields.Values["user.scope"]
explicitScope := hasHookScope && hookScope != ""
derivedScope, err := a.Settings.CreateUserHome(u, a.Server.Root, explicitScope)
if err != nil {
return nil, err
}
log.Printf("user: %s, home dir: [%s].", u.Username, u.Scope)
if err := a.Users.SaveProvisioned(u, derivedScope); err != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@auth/hook.go` around lines 160 - 169, Update the explicit scope detection in
the provisioning flow around a.Fields.Values["user.scope"] so explicitScope is
true only when the hook-provided value is non-empty. Preserve automatic scope
derivation and ensure the resulting SaveProvisioned call performs the
concurrency-safe collision check when user.scope= is empty.

return nil, err
}
} else if p := !users.CheckPwd(a.Cred.Password, u.Password); len(a.Fields.Values) > 1 || p {
Expand Down
68 changes: 68 additions & 0 deletions auth/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"path/filepath"
"runtime"
"testing"

"github.com/filebrowser/filebrowser/v2/settings"
"github.com/filebrowser/filebrowser/v2/users"
)

// writeHookScript writes a POSIX shell script to a temp file and returns its
Expand Down Expand Up @@ -86,3 +89,68 @@ fi
t.Fatalf("expected action %q, got %q", "auth", action)
}
}

// newHookAuth builds a HookAuth for a freshly provisioned user with the given
// parsed hook fields (hook.action=auth is added automatically).
func newHookAuth(store *mockUserStore, s *settings.Settings, srv *settings.Server, username string, fields map[string]string) *HookAuth {
fields["hook.action"] = "auth"
return &HookAuth{
Users: store,
Settings: s,
Server: srv,
Cred: hookCred{Username: username, Password: "a-strong-password"},
Fields: hookFields{Values: fields},
}
}

// With CreateUserDir enabled and no explicit scope from the hook, a provisioned
// hook user must receive its own home directory rather than the server root.
func TestHookSaveUserCreateUserDirIsolatesScope(t *testing.T) {
t.Parallel()

store := &mockUserStore{users: make(map[string]*users.User)}
srv := &settings.Server{Root: t.TempDir()}
s := &settings.Settings{
Key: []byte("key"),
CreateUserDir: true,
UserHomeBasePath: "/users",
Defaults: settings.UserDefaults{
Scope: ".",
Perm: users.Permissions{Create: true},
},
}

u, err := newHookAuth(store, s, srv, "alice", map[string]string{}).SaveUser()
if err != nil {
t.Fatalf("SaveUser error: %v", err)
}
if u.Scope != "/users/alice" {
t.Errorf("hook user without explicit scope: expected /users/alice, got %q", u.Scope)
}
}

// A scope explicitly returned by the hook takes precedence over the automatic
// per-user home directory derivation.
func TestHookSaveUserRespectsExplicitScope(t *testing.T) {
t.Parallel()

store := &mockUserStore{users: make(map[string]*users.User)}
srv := &settings.Server{Root: t.TempDir()}
s := &settings.Settings{
Key: []byte("key"),
CreateUserDir: true,
UserHomeBasePath: "/users",
Defaults: settings.UserDefaults{
Scope: ".",
Perm: users.Permissions{Create: true},
},
}

u, err := newHookAuth(store, s, srv, "teamlead", map[string]string{"user.scope": "/shared/team"}).SaveUser()
if err != nil {
t.Fatalf("SaveUser error: %v", err)
}
if u.Scope != "/shared/team" {
t.Errorf("explicit hook scope should win, got %q", u.Scope)
}
}
9 changes: 3 additions & 6 deletions auth/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,12 @@ func (a ProxyAuth) createUser(usr users.Store, setting *settings.Settings, srv *
user.Perm.Execute = false
user.Commands = []string{}

var userHome string
userHome, err = setting.MakeUserDir(user.Username, user.Scope, srv.Root)
if err != nil {
var derivedScope bool
if derivedScope, err = setting.CreateUserHome(user, srv.Root, false); err != nil {
return nil, err
}
user.Scope = userHome

err = usr.Save(user)
if err != nil {
if err = usr.SaveProvisioned(user, derivedScope); err != nil {
return nil, err
}

Expand Down
Loading
Loading