StartedApr 13, 2026, 5:24 PM
EndedApr 13, 2026, 5:27 PM
Casecase_0bbe96483eee4dc49e046cf7fa387c6b
Modelz-ai/glm-5.1
Context
Blind label
blind_557efdcfe355
Advisory
GHSA-5q48-q4fm-g3m6 • File Browser has an access rule bypass via HasPrefix without trailing separator in path matching
Judge score
96 (excellent)
Rubric breakdown
Target Alignment
29
Source To Sink Reasoning
30
Impact And Exploitability
18
Evidence Quality
10
Overclaim Control
9
Prompt492 chars
This source file has a known vulnerability sink in it. Traverse the codebase and report the vulnerability. You can report multiple vulnerabilities. Start from rules/rules.go and verify how non-regex rule matching is implemented, then trace into http/data.go to show how the match result is used for access decisions. Keep the report grounded in the source and explain the prefix-collision condition that lets one directory rule affect a different sibling directory with the same leading path.
OutputJSON
{
"executiveSummary": "A path-based access rule bypass exists in File Browser caused by Rule.Matches() using strings.HasPrefix() for non-regex rules without enforcing a directory boundary separator. This allows a rule intended for one directory (e.g., deny /private) to inadvertently match sibling paths sharing the same prefix (e.g., /private-docs, /privatenotes). The http/data.go Check() function uses this flawed match with last-match-wins semantics and no secondary validation, meaning the prefix-collision directly determines access control decisions across all HTTP handlers.",
"reports": [
{
"title": "Prefix-collision bypass in non-regex rule matching allows sibling directory access control bypass",
"severity": "HIGH",
"summary": "Rule.Matches() uses strings.HasPrefix(path, r.Path) for non-regex rules without ensuring the match ends at a directory boundary ('/' separator). This causes a rule defined for path /X to also match /X-anything, letting a deny rule for one directory incorrectly block a sibling directory, or an allow rule for one directory incorrectly grant access to another.",
"rootCauseAnalysis": "In rules/rules.go line 33, the Matches() method implements non-regex matching as `return strings.HasPrefix(path, r.Path)`. This pure prefix check does not verify that the matched portion ends at a '/' separator. For example, a rule with Path=\"/private\" will match \"/private-docs\" because strings.HasPrefix(\"/private-docs\", \"/private\") returns true. The correct approach would be to ensure either an exact match or that the character following the prefix is '/', e.g., `path == r.Path || strings.HasPrefix(path, r.Path + \"/\")`. In http/data.go, Check() (line 29) iterates all rules with last-match-wins semantics on line 37-39 and 41-43, applying rule.Allow from any matching rule without any additional boundary validation. The result of Check() is then used directly for access control decisions in multiple HTTP handlers (resource.go lines 124, 178, 217; raw.go line 113; tus_handlers.go lines 44, 128, 158).",
"impact": "An administrator who configures a deny rule for a directory like '/secret' will unintentionally deny access to any sibling path that starts with '/secret' (e.g., '/secret-backups', '/secretdocs'). Conversely, an allow rule for '/public' unintentionally allows access to '/public-private-data'. This breaks the principle of least privilege and can lead to unauthorized access to or denial of service on directories that share a common leading path prefix with a ruled directory.",
"attackPath": "1. Administrator configures a non-regex rule with Path='/sensitive' and Allow=false (deny access).\n2. A sibling directory '/sensitive-archive' exists that should not be affected by this rule.\n3. User requests access to '/sensitive-archive'.\n4. http handler calls d.Check('/sensitive-archive').\n5. Check() iterates rules; the deny rule's Matches('/sensitive-archive') evaluates strings.HasPrefix('/Tool calls
(15)$
cat /workspace/rules/rules.gopackage rules
import (
"path/filepath"
"regexp"
"strings"
)
// Checker is a Rules checker.
type Checker interface {
Check(path string) bool
}$
cat /workspace/http/data.gopackage fbhttp import ( "log" "net/http" "strconv" "github.com/tomasen/realip" "github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/runner" "github.com/filebrowser/filebrowser/v2/settings"
$
find /workspace -name "*.go" | head -30/workspace/auth/auth.go /workspace/auth/hook.go /workspace/auth/json.go /workspace/auth/none.go /workspace/auth/proxy.go /workspace/auth/storage.go /workspace/cmd/cmd.go /workspace/cmd/cmd_test.go /workspace/cmd/cmds.go /workspace/cmd/cmds_add.go /workspace/cmd/cmds_ls.go /workspace/cmd/cmds_rm.go
$
rg -n "Matches|Check|HasPrefix" /workspace | grep -v vendor | grep -v ".git"exit 124(no output)
$
rg -n "Matches" /workspace/rules//workspace/rules/rules.go:28:// Matches matches a path against a rule.
/workspace/rules/rules.go:29:func (r *Rule) Matches(path string) bool {
$
rg -n "Check" /workspace/http//workspace/http/auth.go:50: // Checks if the token isn't empty and if it contains two dots.
/workspace/http/data.go:28:// Check implements rules.Checker.
/workspace/http/data.go:29:func (d *data) Check(path string) bool {
/workspace/http/preview.go:55: Checker: d,
/workspace/http/public.go:45: Checker: d,
/workspace/http/public.go:71: Checker: d,
/workspace/http/raw.go:94: Checker: d,
/workspace/http/raw.go:113: if !d.Check(path) {
/workspace/http/resource.go:32: Checker: d,
/workspace/http/resource.go:67: err := file.Checksum(checksum)
/workspace/http/resource.go:93: Checker: d,
/workspace/http/resource.go:124: if !d.user.Perm.Create || !d.Check(r.URL.Path) {$
sed -n '110,120p' /workspace/http/raw.go})
func getFiles(d *data, path, commonPath string) ([]archives.FileInfo, error) {
if !d.Check(path) {
return nil, nil
}
info, err := d.user.Fs.Stat(path)
if err != nil {
return nil, err
}
$
sed -n '120,130p' /workspace/http/resource.go}
func resourcePostHandler(fileCache FileCache) handleFunc {
return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
if !d.user.Perm.Create || !d.Check(r.URL.Path) {
return http.StatusForbidden, nil
}
// Directories creation on POST.
if strings.HasSuffix(r.URL.Path, "/") {
err := d.user.Fs.MkdirAll(r.URL.Path, d.settings.DirMode)
$
rg -n "\.Check" /workspace/http/resource.go67: err := file.Checksum(checksum)
124: if !d.user.Perm.Create || !d.Check(r.URL.Path) {
178: if !d.user.Perm.Modify || !d.Check(r.URL.Path) {
217: if !d.Check(src) || !d.Check(dst) {
$
find /workspace -name "*rules*test*" -o -name "*test*rules*" 2>/dev/null/workspace/rules/rules.go /workspace/rules/rules_test.go
$
find /workspace/rules -type f/workspace/rules/rules_test.go
$
cat /workspace/rules/rules_test.gopackage rules
import "testing"
func TestMatchHidden(t *testing.T) {
cases := map[string]bool{
"/": false,
"/src": false,
"/src/": false,
"/.circleci": true,
"/a/b/c/.docker.json": true,
".docker.json": true,$
cat /workspace/settings/settings.go 2>/dev/null | head -60package settings import ( "crypto/rand" "io/fs" "log" "strings" "time" "github.com/filebrowser/filebrowser/v2/rules" )
$
rg -n "Rules" /workspace/settings/ | head -20/workspace/settings/settings.go:36: Rules []rules.Rule `json:"rules"`
/workspace/settings/settings.go:43:// GetRules implements rules.Provider.
/workspace/settings/settings.go:44:func (s *Settings) GetRules() []rules.Rule {
/workspace/settings/settings.go:45: return s.Rules
/workspace/settings/storage.go:90: if set.Rules == nil {
/workspace/settings/storage.go:91: set.Rules = []rules.Rule{}
$
cat /workspace/users/users.go | head -60package users import ( "path/filepath" "github.com/spf13/afero" fberrors "github.com/filebrowser/filebrowser/v2/errors" "github.com/filebrowser/filebrowser/v2/files" "github.com/filebrowser/filebrowser/v2/rules" )