# ffuf: a Go web fuzzer for directory, vhost and parameter discovery

> ffuf substitutes the FUZZ keyword into URLs, headers and POST bodies at high request rates. This review covers how it works, how to install it, where it breaks, and who should reach for it instead of Gobuster.

**ffuf/ffuf** — Fast web fuzzer written in Go

- Repository: https://github.com/ffuf/ffuf
- Stars: 16,785 · Forks: 1,611
- Language: Go
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/ffuf-ffuf

## What ffuf solves and who reaches for it

Web content discovery means sending thousands of requests against a host and reading which ones come back differently. Doing that with a shell loop is slow and the output is unreadable. ffuf is a Go binary that takes a wordlist and a URL containing the literal keyword FUZZ, replaces that keyword with each entry, and reports responses that survive your filters. The README frames it as "a fast web fuzzer written in Go" and the examples cover directory discovery, virtual host discovery without DNS records, GET parameter names and values, POST body fields, and JSON payloads mutated by an external program.

The audience is narrow and technical: pentesters, bug bounty hunters and anyone doing authorized reconnaissance against their own or a client's infrastructure. The README points readers at Adam Langley's ffufme container and the hosted ffuf.me for practice, which signals that the project expects you to learn against a target you are allowed to hit. If you need a scanner that decides what is a vulnerability, ffuf is not that. It finds endpoints, parameters and hosts that respond; interpreting those responses is your job.

## The FUZZ keyword and how filtering drives the results

The mechanism is a string substitution with a response filter bolted on top. You place FUZZ wherever the variable belongs. In the URL it fuzzes paths. In a header value it fuzzes the Host header. In a POST body it fuzzes a single field. The README notes that when --input-cmd is used, ffuf displays matches by position and exposes that same position to the called program as the environment variable $FFUF_NUM, which is how the Radamsa example seeds a mutator deterministically.

Filtering is the part that decides whether the tool is usable. A misconfigured filter turns a scan into noise. The README's own examples assume a known baseline: virtual host discovery assumes the default vhost response is 4242 bytes and passes -fs 4242 to drop those; GET parameter fuzzing assumes invalid parameter names return 4242 bytes; value fuzzing assumes a wrong value returns 401 and passes -fc 401. Every one of those is a guess about the target that you have to confirm before the scan means anything. If the application returns a 200 with a generic error page for every unknown path, size filtering will not separate signal from noise and you need a different discriminator.

Time control is split across two flags. -maxtime stops the entire process after a number of seconds. -maxtime-job stops the current job and continues with the next one, and new jobs appear when recursion detects a subdirectory. The README states that without recursion the two flags behave the same, which is a useful detail: people enable -maxtime-job expecting per-request timeouts and get whole-run behaviour instead.

## Installing ffuf and running a first directory scan

The README lists prebuilt binaries on the releases page as the primary route, then package managers. On macOS with Homebrew the command is:

```bash
brew install ffuf
```

On Windows with Scoop or Winget the README gives:

```bash
scoop install ffuf
winget install ffuf.ffuf
```

If you have a recent Go compiler, the README gives this, and notes the same command works for updating:

```bash
go install github.com/ffuf/ffuf/v2@latest
```

Building from a checkout is also documented: clone the repository, then run go get and go build. Note the README's warning here. ffuf depends on Go 1.20 or greater, and a build from a checkout reports git-<date>-<commit> rather than a tag, because Go's embedded build info carries the commit but not the tag name. The authoritative versioned binaries are the ones on the releases page. If you file a bug against a self-built binary, expect to be asked which commit it came from.

A first real use is directory discovery. Put FUZZ at the end of the URL and point -w at a wordlist:

```bash
ffuf -w /path/to/wordlist -u https://target/FUZZ
```

You should see a live progress display and, at the end, a list of paths whose responses did not match your filters. The README's examples use /path/to/wordlist as a placeholder, so you supply the list. If the target returns the same size for every miss, add -fs with that size to suppress the noise. To fuzz a header instead, place FUZZ in the header value:

```bash
ffuf -w /path/to/vhost/wordlist -u https://target -H "Host: FUZZ" -fs 4242
```

To fuzz a POST field, use -X POST with -d and filter the failure status:

```bash
ffuf -w /path/to/postdata.txt -X POST -d "username=admin\&password=FUZZ" -u https://target/login.php -fc 401
```

Defaults can be persisted. The README states ffuf first checks for a default configuration file at $XDG_CONFIG_HOME/ffuf/ffufrc and applies any options set there to every subsequent job. An example file ships in the repository as ffufrc.example. The README does not document how to override or disable that file for a single run, so if a stored default surprises you, the README is silent on the escape hatch.

## Where ffuf is the wrong tool

ffuf ships no wordlist. Every README example takes -w /path/to/wordlist, and the repository contains no bundled lists. A beginner following the tutorial literally will get an error until they find a list elsewhere. The README points to the wiki and to a third-party guide for more extensive documentation, which tells you the core README is a starting point rather than a manual.

Filtering is the second failure mode. The documented examples lean on a stable baseline response size or a failure status code. Targets that return 200 with a soft error page, or that vary response size per request, defeat -fs and -fc. You can move to -mc all and filter on something else, but the README's own mutator example does exactly that and then still needs -fc 400 to remove bad requests. There is no documented content-similarity filter in the README, so if size and status are both unstable, the README offers no further guidance.

Third, this is an HTTP fuzzer. Virtual host discovery via the Host header is documented, but DNS resolution itself is not part of the workflow: the README explicitly frames vhost discovery as working "without DNS records". If your task is enumerating DNS subdomains rather than virtual hosts, ffuf is not the layer you want.

Finally, speed cuts both ways. A fast fuzzer against a production host is a denial-of-service risk. The README documents -maxtime and -maxtime-job as ways to bound a run, and -maxtime-job is described as stopping the current job and continuing with the next, which is not a rate limit. Nothing in the README describes a requests-per-second cap, so the throttle you need is the one you impose on yourself before pointing it at anything you do not own.

## ffuf versus Gobuster: different defaults, different jobs

Gobuster is the comparison people actually search for, and the difference is in defaults rather than in the core idea. Both take a wordlist and a URL. Gobuster ships modes (dir, dns, vhost, fuzz) that you select explicitly, so the tool knows whether it is enumerating directories or DNS names and can apply mode-appropriate output and filtering. ffuf has one substitution model: you decide where FUZZ goes, and the same binary covers directories, headers, parameters and POST bodies. That flexibility is why the README can document vhost discovery and JSON payload fuzzing in the same file.

The practical consequence is that ffuf asks more of you. Gobuster's dir mode has its own notion of what a miss looks like; with ffuf you supply -fs or -fc yourself, and the README's examples show how much of the work that is. In exchange, ffuf covers cases Gobuster's mode list does not: fuzzing a single POST field, driving payloads from an external mutator through --input-cmd, and placing FUZZ in an arbitrary header. If your work is directory and DNS enumeration with conventional settings, Gobuster's defaults save configuration. If your work is arbitrary request-shape fuzzing, ffuf's single model is the more direct fit.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-09. Release v2.3.0 is dated 2026-09-09, with v2.2.1 on 2026-07-13 and v2.2.0 on 2026-07-11, so the project has shipped three releases in roughly two months. That is a real cadence, not a claim about quality.

Upgrading is cheap if you use a package manager or go install, since the README states the same go install command works for updating. It is less cheap if you build from a checkout, because the version string becomes git-<date>-<commit> and you lose the tag. The go.mod pins go 1.20 as the language version and requires a small dependency set: goquery, xdg, brotli, pencode and go-toml. The xdg dependency is what backs the $XDG_CONFIG_HOME/ffuf/ffufrc lookup, and go-toml is what parses that file, so a config file written for one version is parsed by the same TOML library across upgrades. The CHANGELOG.md at the repository root is the place to check before moving versions.

The licence is MIT. That is permissive: you can use, modify and redistribute it, including in commercial work, provided the copyright notice and permission notice are preserved. This is a description of the licence text, not legal advice; if you are redistributing ffuf inside a product, read LICENSE yourself and get counsel if the terms matter to you.

## Conclusion

ffuf suits pentesters and bug bounty hunters who need to fuzz URLs, headers and POST bodies with precise response filtering, and who can supply their own wordlists. It is the wrong tool if you want a tool that ships its own wordlist or a GUI, and it is the wrong tool for blind guessing without a baseline response size or status code to filter against. Before adopting it, verify Go 1.20 or greater if you build from source, confirm the ffufrc path your platform actually resolves to, and check that your target's default response size is stable enough for -fs to be meaningful.

## FAQ

### What is the ffuf command?

ffuf is a web fuzzer written in Go that replaces the literal keyword FUZZ in a URL, header or POST body with each entry from a wordlist. The README's simplest form is ffuf -w /path/to/wordlist -u https://target/FUZZ.

### Which is better, Gobuster or ffuf?

They differ in defaults rather than in the core idea. Gobuster exposes explicit modes for directory and DNS work, while ffuf uses one substitution model where you decide where FUZZ goes, which is what lets it cover vhost headers, single POST fields and external mutators as well.

### How do I fuzz subdomains with ffuf?

The README documents virtual host discovery rather than DNS enumeration: place FUZZ in the Host header with -H "Host: FUZZ" and filter the default response, for example with -fs 4242 when the default vhost response is 4242 bytes.

### Where can I find wordlists for ffuf?

The README does not bundle or recommend a specific wordlist. Every example takes -w /path/to/wordlist as a placeholder, and the README points to the wiki and a third-party guide for more extensive documentation.

## Sources

- [ffuf/ffuf on GitHub](https://github.com/ffuf/ffuf)
- [Issues](https://github.com/ffuf/ffuf/issues)
- [License: MIT](https://github.com/ffuf/ffuf/blob/master/LICENSE)
- [README](https://github.com/ffuf/ffuf/blob/master/README.md)
- [Releases](https://github.com/ffuf/ffuf/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ffuf-ffuf
