diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml
index 1f60f78c..f8e358c0 100644
--- a/.github/workflows/auto-merge-dependabot.yml
+++ b/.github/workflows/auto-merge-dependabot.yml
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@v2.4.0
+ uses: dependabot/fetch-metadata@v3.1.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 878f30ce..db58fd2d 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -17,23 +17,23 @@ jobs:
steps:
- name: checkout sources
- uses: actions/checkout@v5
+ uses: actions/checkout@v7
- name: Set up QEMU
- uses: docker/setup-qemu-action@v3
+ uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386,linux/ppc64le
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index b659900e..4211b6bd 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v5
+ uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
@@ -16,7 +16,7 @@ jobs:
go-version: "stable"
- name: Install Task
- uses: arduino/setup-task@v2
+ uses: go-task/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index d6fcd95b..0e18a7a2 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -8,14 +8,14 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- uses: actions/setup-go@v6
with:
go-version: "stable"
- name: golangci-lint
- uses: golangci/golangci-lint-action@v8
+ uses: golangci/golangci-lint-action@v9
with:
version: latest
args: --timeout=5m
diff --git a/.github/workflows/hadolint.yml b/.github/workflows/hadolint.yml
index d59735f8..48966a58 100644
--- a/.github/workflows/hadolint.yml
+++ b/.github/workflows/hadolint.yml
@@ -12,8 +12,8 @@ jobs:
name: hadolint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
- - uses: hadolint/hadolint-action@v3.2.0
+ - uses: actions/checkout@v7
+ - uses: hadolint/hadolint-action@v3.3.0
with:
dockerfile: Dockerfile
# DL3007: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 87f70332..67c44262 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -26,7 +26,7 @@ jobs:
go-version: "stable"
- name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v6.4.0
+ uses: goreleaser/goreleaser-action@v7.2.2
with:
distribution: goreleaser
version: latest
diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml
index edeff029..c645811e 100644
--- a/.github/workflows/update.yml
+++ b/.github/workflows/update.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
with:
ref: dev
@@ -28,7 +28,7 @@ jobs:
go mod tidy
- name: Commit and push changes
- uses: stefanzweifel/git-auto-commit-action@v5
+ uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "chore: update dependencies [automated]"
branch: dev
diff --git a/.github/workflows/vhs.yml b/.github/workflows/vhs.yml
index 2b23a88a..229094c4 100644
--- a/.github/workflows/vhs.yml
+++ b/.github/workflows/vhs.yml
@@ -3,6 +3,10 @@ on:
push:
paths:
- vhs/**.tape
+ schedule:
+ # every week
+ - cron: "0 0 * * 0"
+ workflow_dispatch:
permissions:
contents: write
@@ -11,7 +15,7 @@ jobs:
vhs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v6
@@ -19,7 +23,7 @@ jobs:
go-version: "stable"
- name: Install Task
- uses: arduino/setup-task@v2
+ uses: go-task/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -29,14 +33,18 @@ jobs:
- name: Build linux
run: task linux
- - name: Install deps
+ - name: Install vhs deps
run: |
sudo apt update
sudo apt install -y ffmpeg ttyd
- - uses: charmbracelet/vhs-action@v2
- with:
- path: "vhs/gobuster_dir.tape"
+ - name: Install vhs
+ run: |
+ go install github.com/charmbracelet/vhs@latest
+
+ - name: Generate vhs gif
+ run: |
+ vhs vhs/gobuster_dir.tape -o vhs/gobuster_dir.gif
- name: commit and push changes
run: |
@@ -44,4 +52,4 @@ jobs:
git config user.email "<>"
git add vhs/*.gif
git commit -m "update vhs gifs" || echo "no changes to commit"
- git push origin master
+ git push
diff --git a/.github/workflows/yamllint.yml b/.github/workflows/yamllint.yml
index d58ce73a..9693d7df 100644
--- a/.github/workflows/yamllint.yml
+++ b/.github/workflows/yamllint.yml
@@ -13,7 +13,7 @@ jobs:
name: yamllint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@v7
- uses: karancode/yamllint-github-action@master
with:
# fail on warnings and errors
diff --git a/.gitignore b/.gitignore
index 3bbfe4b5..a8bf3b57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ config.json
gobuster
*.txt
dist/
+wordlist
\ No newline at end of file
diff --git a/.golangci.yml b/.golangci.yml
index 51495629..859d2f4e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -23,7 +23,6 @@ linters:
- gocheckcompilerdirectives
- gochecknoinits
- gochecksumtype
- - goconst
- gocritic
- gomoddirectives
- goprintffuncname
@@ -159,7 +158,6 @@ linters:
- linters:
- bodyclose
- errcheck
- - goconst
- gosec
- noctx
- wrapcheck
diff --git a/README.md b/README.md
index b00383c1..fb22a14c 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,13 @@ gobuster dir -u https://example.com -w wordlist.txt -l
# Filter by status codes
gobuster dir -u https://example.com -w wordlist.txt -s 200,301,302
+
+# Filter using a regex against the response body
+# This can be handy for websites that return status code 200 for everything, but the html contains an error message
+gobuster dir -u https://example.com -w wordlist.txt -re "error\shello"
+
+# Filter using a regex but inverted against the response body
+gobuster dir -u https://example.com -w wordlist.txt -rei "(?i)\berror\b"
```
#### 🔍 DNS Mode (`dns`)
@@ -344,6 +351,18 @@ _Remember: Always test responsibly and with proper authorization._
+3.8.3
+
+## 3.8.3
+
+- Add option to filter body by regex
+- Add option to save response bodies
+- Allow comma in Header values passed via the CLI
+
+
+
+
+
3.8.2
## 3.8.2
diff --git a/cli/dir/dir.go b/cli/dir/dir.go
index ca46f331..588a7fbe 100644
--- a/cli/dir/dir.go
+++ b/cli/dir/dir.go
@@ -3,6 +3,7 @@ package dir
import (
"errors"
"fmt"
+ "regexp"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterdir"
@@ -36,11 +37,19 @@ func getFlags() []cli.Flag {
&cli.BoolFlag{Name: "discover-backup", Aliases: []string{"db"}, Value: false, Usage: "Upon finding a file search for backup files by appending multiple backup extensions"},
&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
&cli.BoolFlag{Name: "force", Value: false, Usage: "Continue even if the prechecks fail. Please only use this if you know what you are doing, it can lead to unexpected results."},
+ &cli.StringFlag{Name: "regex", Aliases: []string{"re"}, Usage: "Use regex to filter the results, by inspecting the content of the response body. When using this option be sure to set the status-codes and status-codes-blacklist options accordingly. The regex check is done after the status code checks. Only responses matching the regex will be displayed."},
+ &cli.StringFlag{Name: "regex-invert", Aliases: []string{"rei"}, Usage: "Use regex to filter the results, but inverted, by inspecting the content of the response body. When using this option be sure to set the status-codes and status-codes-blacklist options accordingly. The regex check is done after the status code checks. Only responses NOT matching the regex will be displayed."},
}...)
return flags
}
func run(c *cli.Context) error {
+ globalOpts, err := internalcli.ParseGlobalOptions(c)
+ if err != nil {
+ return err
+ }
+ log := libgobuster.NewLogger(globalOpts.Debug)
+
pluginOpts := gobusterdir.NewOptions()
httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
@@ -101,12 +110,26 @@ func run(c *cli.Context) error {
}
pluginOpts.ExcludeLengthParsed = ret4
- globalOpts, err := internalcli.ParseGlobalOptions(c)
- if err != nil {
- return err
+ if c.IsSet("regex") && c.IsSet("regex-invert") {
+ return errors.New("regex and regex-invert are mutually exclusive, please set only one")
}
- log := libgobuster.NewLogger(globalOpts.Debug)
+ if c.IsSet("regex") && c.String("regex") != "" {
+ regex, err := regexp.Compile(c.String("regex"))
+ if err != nil {
+ return fmt.Errorf("invalid value for regex: %w", err)
+ }
+ pluginOpts.Regex = regex
+ }
+
+ if c.IsSet("regex-invert") && c.String("regex-invert") != "" {
+ regex, err := regexp.Compile(c.String("regex-invert"))
+ if err != nil {
+ return fmt.Errorf("invalid value for regex-invert: %w", err)
+ }
+ pluginOpts.Regex = regex
+ pluginOpts.RegexInvert = true
+ }
plugin, err := gobusterdir.New(&globalOpts, pluginOpts, log)
if err != nil {
diff --git a/cli/options.go b/cli/options.go
index 7f035744..95302578 100644
--- a/cli/options.go
+++ b/cli/options.go
@@ -129,7 +129,8 @@ func CommonHTTPOptions() []cli.Flag {
&cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"},
&cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"},
&cli.BoolFlag{Name: "no-canonicalize-headers", Aliases: []string{"nch"}, Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"},
- &cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"},
+ &cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "Specify HTTP method"},
+ &cli.StringFlag{Name: "body-output-dir", Usage: "Directory to store response bodies"},
}...)
flags = append(flags, BasicHTTPOptions()...)
return flags
@@ -209,6 +210,14 @@ func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) {
opts.Headers = append(opts.Headers, header)
}
+ if c.IsSet("body-output-dir") {
+ opts.BodyOutputDir = c.String("body-output-dir")
+ err = os.MkdirAll(opts.BodyOutputDir, 0o755)
+ if err != nil {
+ return opts, fmt.Errorf("could not create body output dir %q: %w", opts.BodyOutputDir, err)
+ }
+ }
+
return opts, nil
}
diff --git a/go.mod b/go.mod
index aa3bb380..a56a5ce1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,31 +1,30 @@
module github.com/OJ/gobuster/v3
-go 1.25
+go 1.25.0
require (
- github.com/fatih/color v1.18.0
+ github.com/fatih/color v1.19.0
github.com/google/uuid v1.6.0
- github.com/pin/tftp/v3 v3.1.0
+ github.com/pin/tftp/v3 v3.2.0
github.com/urfave/cli/v2 v2.27.7
go.uber.org/automaxprocs v1.6.0
- golang.org/x/term v0.37.0
- software.sslmate.com/src/go-pkcs12 v0.6.0
+ golang.org/x/term v0.44.0
+ software.sslmate.com/src/go-pkcs12 v0.7.3
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
- github.com/google/go-cmp v0.7.0 // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-colorable v0.1.15 // indirect
+ github.com/mattn/go-isatty v0.0.22 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
- golang.org/x/crypto v0.45.0 // indirect
- golang.org/x/mod v0.27.0 // indirect
- golang.org/x/net v0.47.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/sys v0.38.0 // indirect
- golang.org/x/tools v0.36.0 // indirect
- mvdan.cc/gofumpt v0.9.0 // indirect
+ golang.org/x/crypto v0.53.0 // indirect
+ golang.org/x/mod v0.36.0 // indirect
+ golang.org/x/net v0.56.0 // indirect
+ golang.org/x/sync v0.20.0 // indirect
+ golang.org/x/sys v0.46.0 // indirect
+ golang.org/x/tools v0.45.0 // indirect
+ mvdan.cc/gofumpt v0.10.0 // indirect
)
tool mvdan.cc/gofumpt
diff --git a/go.sum b/go.sum
index 7d3c5f60..9f94df5e 100644
--- a/go.sum
+++ b/go.sum
@@ -2,10 +2,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
-github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
-github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
-github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
+github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
+github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -14,12 +14,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
-github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
+github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/pin/tftp/v3 v3.2.0 h1:q6K5G6T0TA7e3wDJsB/7VpD3iaWwVEJD/nEuh3q9Sk0=
+github.com/pin/tftp/v3 v3.2.0/go.mod h1:qc5ySXB5aOS1H6ULneqB4g5nshqV1CgeV/l/M6rEDms=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
@@ -36,28 +36,23 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
-golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
-golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
-golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
-golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
-golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
-golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
-golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
+golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
+golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
+golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
+golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
+golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-mvdan.cc/gofumpt v0.9.0 h1:W0wNHMSvDBDIyZsm3nnGbVfgp5AknzBrGJnfLCy501w=
-mvdan.cc/gofumpt v0.9.0/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
-software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
-software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
+mvdan.cc/gofumpt v0.10.0 h1:yGGpRS2pBN2OQIi7b21IXknJna7faPkFaVfHLrN6Euo=
+mvdan.cc/gofumpt v0.10.0/go.mod h1:sU2ElXHzOEmvoPqfutYG7uunlueR4K2T1JFml40SzP4=
+software.sslmate.com/src/go-pkcs12 v0.7.3 h1:JBQD3FDqYjTeyDAeZQklj2ar88ykBLtALloPJHyAauU=
+software.sslmate.com/src/go-pkcs12 v0.7.3/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
diff --git a/gobusterdir/gobusterdir.go b/gobusterdir/gobusterdir.go
index 4f1d8f37..bd683711 100644
--- a/gobusterdir/gobusterdir.go
+++ b/gobusterdir/gobusterdir.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
"strings"
"syscall"
"text/tabwriter"
@@ -40,7 +41,7 @@ func (e *WildcardError) Error() string {
} else {
addInfo = fmt.Sprintf("%s => %d (Length: %d)", e.url, e.statusCode, e.length)
}
- return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length or the status code or set the wildcard option.", addInfo)
+ return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length (or as a range), the status code or set the force option (but expect false positives).", addInfo)
}
// GobusterDir is the main type to implement the interface
@@ -86,6 +87,7 @@ func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
+ BodyOutputDir: opts.BodyOutputDir,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
@@ -102,6 +104,18 @@ func (d *GobusterDir) Name() string {
return "directory enumeration"
}
+func (d *GobusterDir) regexBodyIsAMatch(body []byte) bool {
+ switch {
+ case d.options.Regex != nil && body != nil:
+ if d.options.RegexInvert {
+ return !d.options.Regex.Match(body)
+ }
+ return d.options.Regex.Match(body)
+ default:
+ return true
+ }
+}
+
// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) error {
// add trailing slash
@@ -139,7 +153,7 @@ func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) erro
url.Path = fmt.Sprintf("%s/", url.Path)
}
- wildcardResp, wildcardLength, wildcardHeader, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
+ wildcardResp, wildcardLength, wildcardHeader, wildcardBody, err := d.http.Request(ctx, url, libgobuster.RequestOptions{ReturnBody: true})
if err != nil {
var retErr error
switch {
@@ -170,10 +184,16 @@ func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) erro
switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
+ if d.regexBodyIsAMatch(wildcardBody) {
+ return nil
+ }
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(wildcardResp) {
+ if d.regexBodyIsAMatch(wildcardBody) {
+ return nil
+ }
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
default:
@@ -252,9 +272,16 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
var statusCode int
var size int64
var header http.Header
+ var body []byte
+
+ requestOptions := libgobuster.RequestOptions{}
+ if d.options.Regex != nil || d.options.BodyOutputDir != "" {
+ requestOptions.ReturnBody = true
+ }
+
for i := 1; i <= tries; i++ {
var err error
- statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{})
+ statusCode, size, header, body, err = d.http.Request(ctx, url, requestOptions)
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
@@ -280,17 +307,29 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
break
}
+ if d.options.BodyOutputDir != "" && body != nil {
+ fname := libgobuster.SanitizeFilename(fmt.Sprintf("%s_%d.html", strings.Trim(entity, "/"), statusCode))
+ fpath := filepath.Join(d.options.BodyOutputDir, fname)
+ err := os.WriteFile(fpath, body, 0o600) // nolint:gosec
+ if err != nil {
+ progress.MessageChan <- libgobuster.Message{
+ Level: libgobuster.LevelError,
+ Message: fmt.Sprintf("Could not write body to file %s: %v", fpath, err),
+ }
+ }
+ }
+
if statusCode != 0 {
resultStatus := false
switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
- resultStatus = true
+ resultStatus = d.regexBodyIsAMatch(body)
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(statusCode) {
- resultStatus = true
+ resultStatus = d.regexBodyIsAMatch(body)
}
default:
return nil, errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
@@ -448,6 +487,18 @@ func (d *GobusterDir) GetConfigString() (string, error) {
}
}
+ if o.Regex != nil {
+ if o.RegexInvert {
+ if _, err := fmt.Fprintf(tw, "[+] Regex Inverted:\t%s\n", o.Regex.String()); err != nil {
+ return "", err
+ }
+ } else {
+ if _, err := fmt.Fprintf(tw, "[+] Regex:\t%s\n", o.Regex.String()); err != nil {
+ return "", err
+ }
+ }
+ }
+
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
diff --git a/gobusterdir/options.go b/gobusterdir/options.go
index 3e1bdb59..1785b3ce 100644
--- a/gobusterdir/options.go
+++ b/gobusterdir/options.go
@@ -1,6 +1,8 @@
package gobusterdir
import (
+ "regexp"
+
"github.com/OJ/gobuster/v3/libgobuster"
)
@@ -22,6 +24,8 @@ type OptionsDir struct {
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
Force bool
+ Regex *regexp.Regexp
+ RegexInvert bool
}
// NewOptions returns a new initialized OptionsDir
diff --git a/gobusterfuzz/gobusterfuzz.go b/gobusterfuzz/gobusterfuzz.go
index a5cb028f..f30c55d0 100644
--- a/gobusterfuzz/gobusterfuzz.go
+++ b/gobusterfuzz/gobusterfuzz.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
"strings"
"syscall"
"text/tabwriter"
@@ -52,13 +53,15 @@ func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *libgobuster
}
basicOptions := libgobuster.BasicHTTPOptions{
- Proxy: opts.Proxy,
- Timeout: opts.Timeout,
- UserAgent: opts.UserAgent,
- NoTLSValidation: opts.NoTLSValidation,
- RetryOnTimeout: opts.RetryOnTimeout,
- RetryAttempts: opts.RetryAttempts,
- TLSCertificate: opts.TLSCertificate,
+ Proxy: opts.Proxy,
+ Timeout: opts.Timeout,
+ UserAgent: opts.UserAgent,
+ NoTLSValidation: opts.NoTLSValidation,
+ RetryOnTimeout: opts.RetryOnTimeout,
+ RetryAttempts: opts.RetryAttempts,
+ TLSCertificate: opts.TLSCertificate,
+ LocalAddr: opts.LocalAddr,
+ TLSRenegotiation: opts.TLSRenegotiation,
}
httpOpts := libgobuster.HTTPOptions{
@@ -70,6 +73,7 @@ func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *libgobuster
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
+ BodyOutputDir: opts.BodyOutputDir,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
@@ -139,6 +143,10 @@ func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *l
requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word)
}
+ if d.options.BodyOutputDir != "" {
+ requestOptions.ReturnBody = true
+ }
+
// add some debug output
if d.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
@@ -156,9 +164,10 @@ func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *l
var statusCode int
var size int64
var responseHeaders http.Header
+ var body []byte
for i := 1; i <= tries; i++ {
var err error
- statusCode, size, responseHeaders, _, err = d.http.Request(ctx, url, requestOptions)
+ statusCode, size, responseHeaders, body, err = d.http.Request(ctx, url, requestOptions)
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
@@ -185,6 +194,18 @@ func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *l
break
}
+ if d.options.BodyOutputDir != "" && body != nil {
+ fname := libgobuster.SanitizeFilename(fmt.Sprintf("%s_%d.html", strings.Trim(word, "/"), statusCode))
+ fpath := filepath.Join(d.options.BodyOutputDir, fname)
+ err := os.WriteFile(fpath, body, 0o600) // nolint:gosec
+ if err != nil {
+ progress.MessageChan <- libgobuster.Message{
+ Level: libgobuster.LevelError,
+ Message: fmt.Sprintf("Could not write body to file %s: %v", fpath, err),
+ }
+ }
+ }
+
if statusCode != 0 {
resultStatus := true
diff --git a/gobustergcs/gobustersgcs.go b/gobustergcs/gobustersgcs.go
index 54bda375..469b41a0 100644
--- a/gobustergcs/gobustersgcs.go
+++ b/gobustergcs/gobustersgcs.go
@@ -43,13 +43,15 @@ func New(globalopts *libgobuster.Options, opts *OptionsGCS, logger *libgobuster.
}
basicOptions := libgobuster.BasicHTTPOptions{
- Proxy: opts.Proxy,
- Timeout: opts.Timeout,
- UserAgent: opts.UserAgent,
- NoTLSValidation: opts.NoTLSValidation,
- RetryOnTimeout: opts.RetryOnTimeout,
- RetryAttempts: opts.RetryAttempts,
- TLSCertificate: opts.TLSCertificate,
+ Proxy: opts.Proxy,
+ Timeout: opts.Timeout,
+ UserAgent: opts.UserAgent,
+ NoTLSValidation: opts.NoTLSValidation,
+ RetryOnTimeout: opts.RetryOnTimeout,
+ RetryAttempts: opts.RetryAttempts,
+ TLSCertificate: opts.TLSCertificate,
+ LocalAddr: opts.LocalAddr,
+ TLSRenegotiation: opts.TLSRenegotiation,
}
httpOpts := libgobuster.HTTPOptions{
diff --git a/gobustervhost/gobustervhost.go b/gobustervhost/gobustervhost.go
index f2bae76d..113d7744 100644
--- a/gobustervhost/gobustervhost.go
+++ b/gobustervhost/gobustervhost.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
+ "path/filepath"
"strings"
"sync"
"syscall"
@@ -45,13 +46,15 @@ func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *libgobuste
}
basicOptions := libgobuster.BasicHTTPOptions{
- Proxy: opts.Proxy,
- Timeout: opts.Timeout,
- UserAgent: opts.UserAgent,
- NoTLSValidation: opts.NoTLSValidation,
- RetryOnTimeout: opts.RetryOnTimeout,
- RetryAttempts: opts.RetryAttempts,
- TLSCertificate: opts.TLSCertificate,
+ Proxy: opts.Proxy,
+ Timeout: opts.Timeout,
+ UserAgent: opts.UserAgent,
+ NoTLSValidation: opts.NoTLSValidation,
+ RetryOnTimeout: opts.RetryOnTimeout,
+ RetryAttempts: opts.RetryAttempts,
+ TLSCertificate: opts.TLSCertificate,
+ LocalAddr: opts.LocalAddr,
+ TLSRenegotiation: opts.TLSRenegotiation,
}
httpOpts := libgobuster.HTTPOptions{
@@ -63,6 +66,7 @@ func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *libgobuste
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
+ BodyOutputDir: opts.BodyOutputDir,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
@@ -197,6 +201,18 @@ func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *
break
}
+ if v.options.BodyOutputDir != "" && body != nil {
+ fname := libgobuster.SanitizeFilename(fmt.Sprintf("%s_%d.html", strings.Trim(word, "/"), statusCode))
+ fpath := filepath.Join(v.options.BodyOutputDir, fname)
+ err := os.WriteFile(fpath, body, 0o600) // nolint:gosec
+ if err != nil {
+ progress.MessageChan <- libgobuster.Message{
+ Level: libgobuster.LevelError,
+ Message: fmt.Sprintf("Could not write body to file %s: %v", fpath, err),
+ }
+ }
+ }
+
// subdomain must not match default vhost and non existent vhost
// or verbose mode is enabled
found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody)
diff --git a/libgobuster/helpers.go b/libgobuster/helpers.go
index 4acd82f3..6e7e0b2c 100644
--- a/libgobuster/helpers.go
+++ b/libgobuster/helpers.go
@@ -7,9 +7,11 @@ import (
"fmt"
"io"
"os"
+ "path/filepath"
"regexp"
"strconv"
"strings"
+ "unicode"
)
// Set is a set of Ts
@@ -84,7 +86,7 @@ func lineCounter(r io.Reader) (int, error) {
// store last character received if we got any bytes
if c > 0 {
- lastChar = buf[c-1]
+ lastChar = buf[c-1] // nolint:gosec
}
switch {
@@ -207,3 +209,71 @@ func ParseCommaSeparatedInt(inputString string) (Set[int], error) {
}
return ret, nil
}
+
+// Windows reserved characters: < > : " | ? * and control characters (0-31)
+var filenameInvalidChars = regexp.MustCompile(`[<>:"|?*\x00-\x1f]`)
+
+// sanitizeFilename removes or replaces invalid characters from a filename
+// to make it safe for use on Windows, macOS, and Linux filesystems
+func SanitizeFilename(filename string) string {
+ if filename == "" {
+ return "unnamed"
+ }
+
+ // Remove leading/trailing whitespace
+ filename = strings.TrimSpace(filename)
+
+ // Replace path separators and other problematic characters
+ filename = strings.ReplaceAll(filename, "/", "_")
+ filename = strings.ReplaceAll(filename, "\\", "_")
+
+ filename = filenameInvalidChars.ReplaceAllString(filename, "_")
+
+ // Remove non-printable Unicode characters
+ filename = strings.Map(func(r rune) rune {
+ if unicode.IsPrint(r) {
+ return r
+ }
+ return '_'
+ }, filename)
+
+ // Windows reserved names (case-insensitive)
+ reservedNames := []string{
+ "CON", "PRN", "AUX", "NUL",
+ "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
+ "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
+ }
+
+ // Check if filename (without extension) is a reserved name
+ nameOnly := strings.TrimSuffix(filename, filepath.Ext(filename))
+ for _, reserved := range reservedNames {
+ if strings.EqualFold(nameOnly, reserved) {
+ filename = "_" + filename
+ break
+ }
+ }
+
+ // Remove trailing dots and spaces (Windows requirement)
+ filename = strings.TrimRight(filename, ". ")
+
+ // Ensure filename isn't empty after sanitization
+ if filename == "" {
+ filename = "unnamed"
+ }
+
+ filename = filepath.Base(filename)
+
+ // Limit length to 255 characters (common filesystem limit)
+ if len(filename) > 255 {
+ ext := filepath.Ext(filename)
+ base := strings.TrimSuffix(filename, ext)
+ maxBase := 255 - len(ext)
+ if maxBase > 0 {
+ filename = base[:maxBase] + ext
+ } else {
+ filename = filename[:255]
+ }
+ }
+
+ return filename
+}
diff --git a/libgobuster/helpers_test.go b/libgobuster/helpers_test.go
index f2f39d04..c84ce254 100644
--- a/libgobuster/helpers_test.go
+++ b/libgobuster/helpers_test.go
@@ -392,3 +392,173 @@ func BenchmarkParseCommaSeparatedInt(b *testing.B) {
})
}
}
+
+func TestSanitizeFilename(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ expected string
+ }{
+ {
+ name: "empty string",
+ input: "",
+ expected: "unnamed",
+ },
+ {
+ name: "normal filename",
+ input: "test.txt",
+ expected: "test.txt",
+ },
+ {
+ name: "filename with spaces",
+ input: " test file.txt ",
+ expected: "test file.txt",
+ },
+ {
+ name: "filename with path separators",
+ input: "folder/test\\file.txt",
+ expected: "folder_test_file.txt",
+ },
+ {
+ name: "filename with Windows invalid characters",
+ input: "testname:with|invalid?chars*.txt",
+ expected: "test_file_name_with_invalid_chars_.txt",
+ },
+ {
+ name: "filename with control characters",
+ input: "test\x00file\x1fname.txt",
+ expected: "test_file_name.txt",
+ },
+ {
+ name: "filename with non-printable Unicode",
+ input: "test\u200bfile\u2028name.txt",
+ expected: "test_file_name.txt",
+ },
+ {
+ name: "Windows reserved name - CON",
+ input: "CON.txt",
+ expected: "_CON.txt",
+ },
+ {
+ name: "Windows reserved name - PRN (lowercase)",
+ input: "prn.log",
+ expected: "_prn.log",
+ },
+ {
+ name: "Windows reserved name - COM1",
+ input: "COM1",
+ expected: "_COM1",
+ },
+ {
+ name: "Windows reserved name - LPT9",
+ input: "lpt9.dat",
+ expected: "_lpt9.dat",
+ },
+ {
+ name: "filename with reserved name as part of longer name",
+ input: "CONfig.txt",
+ expected: "CONfig.txt",
+ },
+ {
+ name: "filename with trailing dots and spaces",
+ input: "test.txt.. ",
+ expected: "test.txt",
+ },
+ {
+ name: "filename that becomes empty after sanitization",
+ input: "... ",
+ expected: "unnamed",
+ },
+ {
+ name: "very long filename",
+ input: strings.Repeat("a", 300) + ".txt",
+ expected: strings.Repeat("a", 251) + ".txt", // 255 total
+ },
+ {
+ name: "long filename with long extension",
+ input: strings.Repeat("a", 250) + "." + strings.Repeat("b", 10),
+ expected: strings.Repeat("a", 244) + "." + strings.Repeat("b", 10), // 255 total
+ },
+ {
+ name: "long filename where extension is too long",
+ input: "test." + strings.Repeat("b", 260),
+ expected: ("test." + strings.Repeat("b", 260))[:255],
+ },
+ {
+ name: "whitespace only",
+ input: " \t\n ",
+ expected: "unnamed",
+ },
+ {
+ name: "mixed invalid characters and reserved name",
+ input: "aux|withchars.log",
+ expected: "aux_with_invalid_chars.log",
+ },
+ {
+ name: "reserved name",
+ input: "AUX.log",
+ expected: "_AUX.log",
+ },
+ {
+ name: "Unicode filename",
+ input: "тест файл.txt",
+ expected: "тест файл.txt",
+ },
+ {
+ name: "filename with quotes",
+ input: `"quoted filename".txt`,
+ expected: "_quoted filename_.txt",
+ },
+ {
+ name: "filename with pipe character",
+ input: "file|with|pipes.txt",
+ expected: "file_with_pipes.txt",
+ },
+ {
+ name: "filename with question marks",
+ input: "what?is?this?.txt",
+ expected: "what_is_this_.txt",
+ },
+ {
+ name: "filename with asterisks",
+ input: "wild*card*name*.txt",
+ expected: "wild_card_name_.txt",
+ },
+ {
+ name: "only extension",
+ input: ".hidden",
+ expected: ".hidden",
+ },
+ {
+ name: "reserved name without extension",
+ input: "NUL",
+ expected: "_NUL",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := SanitizeFilename(tt.input)
+ if result != tt.expected {
+ t.Errorf("sanitizeFilename(%q) = %q, want %q", tt.input, result, tt.expected)
+ }
+
+ // Additional validation: ensure result is safe
+ if len(result) > 255 {
+ t.Errorf("sanitizeFilename(%q) returned filename too long: %d characters", tt.input, len(result))
+ }
+
+ if strings.ContainsAny(result, `<>:"|?*`) {
+ t.Errorf("sanitizeFilename(%q) still contains invalid characters: %q", tt.input, result)
+ }
+
+ if strings.Contains(result, "/") || strings.Contains(result, "\\") {
+ t.Errorf("sanitizeFilename(%q) still contains path separators: %q", tt.input, result)
+ }
+
+ if result == "" {
+ t.Errorf("sanitizeFilename(%q) returned empty string", tt.input)
+ }
+ })
+ }
+}
diff --git a/libgobuster/http.go b/libgobuster/http.go
index 9a80e0a5..2e97bad0 100644
--- a/libgobuster/http.go
+++ b/libgobuster/http.go
@@ -226,7 +226,7 @@ func (client *HTTPClient) makeRequest(ctx context.Context, fullURL url.URL, opts
client.logger.Debugf("%s", dump)
}
- resp, err := client.client.Do(req)
+ resp, err := client.client.Do(req) // nolint:gosec
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
diff --git a/libgobuster/options_http.go b/libgobuster/options_http.go
index f4dcbbe6..e50e497a 100644
--- a/libgobuster/options_http.go
+++ b/libgobuster/options_http.go
@@ -23,7 +23,7 @@ type BasicHTTPOptions struct {
// HTTPOptions is the struct to pass in all http options to Gobuster
type HTTPOptions struct {
BasicHTTPOptions
- Password string
+ Password string // nolint:gosec
URL *url.URL
Username string
Cookies string
@@ -31,4 +31,5 @@ type HTTPOptions struct {
NoCanonicalizeHeaders bool
FollowRedirect bool
Method string
+ BodyOutputDir string
}
diff --git a/libgobuster/version.go b/libgobuster/version.go
index 69134730..51a56265 100644
--- a/libgobuster/version.go
+++ b/libgobuster/version.go
@@ -8,7 +8,7 @@ import (
const (
// VERSION contains the current gobuster version
- VERSION = "3.8.2"
+ VERSION = "3.8.3"
)
func GetVersion() string {
diff --git a/main.go b/main.go
index 08ce1ae1..bb300dd5 100644
--- a/main.go
+++ b/main.go
@@ -54,6 +54,7 @@ func main() {
s3.Command(),
gcs.Command(),
},
+ DisableSliceFlagSeparator: true, // needed so we can specify ',' in slice flags. Otherwise urfave/cli splits on ','
}
err := app.Run(os.Args)
diff --git a/vhs/gobuster_dir.tape b/vhs/gobuster_dir.tape
index 2b4e8ecb..04766c3a 100644
--- a/vhs/gobuster_dir.tape
+++ b/vhs/gobuster_dir.tape
@@ -55,7 +55,7 @@
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
-Output vhs/gobuster_dir.gif
+Output gobuster_dir.gif
# Require gobuster
diff --git a/vhs/server.go b/vhs/server.go
index 749fa97f..c1a5e4be 100644
--- a/vhs/server.go
+++ b/vhs/server.go
@@ -41,8 +41,8 @@ func main() {
})})
x.routes = append(x.routes, &route{regexp.MustCompile(`^/`), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
- if _, err := w.Write([]byte(r.URL.Path)); err != nil {
- log.Fatal(err.Error())
+ if _, err := w.Write([]byte(r.URL.Path)); err != nil { // nolint:gosec
+ log.Fatal(err.Error()) // nolint:gosec
}
})})