The NGINX length hiding module is often recommended as a mitigation for the BREACH compression attack. However, security research has demonstrated that random padding does NOT prevent BREACH, it only slows down the attack slightly. This article explains how the module works, why it’s insufficient, and what actually protects against BREACH in 2026.
Important Disclosure: Random length padding has been proven ineffective at preventing BREACH attacks. The module at most slows down attackers, who can bypass the randomization through statistical analysis. Modern browser protections (SameSite cookies) and application-level mitigations (CSRF token rotation) provide far better protection.
The BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext) attack was discovered in 2013. It is tracked as CVE-2013-3587. This attack allows attackers to extract sensitive data like CSRF tokens, session identifiers, and API keys from compressed HTTPS responses by observing subtle changes in response sizes to guess secrets one character at a time.
Understanding the BREACH Attack
BREACH exploits a fundamental property of compression algorithms like gzip and deflate. When data contains repeated sequences, the compressed output becomes smaller. An attacker who can inject content into a compressed response containing secrets can systematically guess those secrets by observing size changes.
For example, if an HTML page contains a CSRF token csrf_token=abc123, an attacker might inject csrf_token=a through a form field or URL parameter. If this injection causes the compressed response to become smaller, the attacker knows the token starts with “a”. Repeating this process reveals the entire secret.
Conditions for BREACH Vulnerability
Your application is vulnerable to BREACH when all three conditions are present simultaneously:
- HTTP compression is enabled (gzip or deflate in
Accept-Encoding) - User input is reflected in responses (search terms, form values, URL parameters)
- Secrets appear in the response body (CSRF tokens, API keys, session data)
Why Length Hiding Does NOT Prevent BREACH
The theory behind length hiding is that adding random-length padding makes it impossible for attackers to correlate size changes with their guesses. However, this has been proven wrong.
From the original BREACH paper:
“While this measure does make the attack take longer, it does so only slightly… By repeating requests and averaging the sizes of the corresponding responses, the attacker can quickly learn the true length of the cipher text.”
The nginx issue tracker confirms this:
“It was demonstrated more than once that padding cannot prevent the BREACH attack. The random padding at most slows down the attack.”
The Statistical Bypass
The bypass works because of basic statistics. Random padding adds noise, but noise can be filtered out by making more requests and averaging the results. The standard error of the mean is inversely proportional to √N, so attackers simply make more requests to filter out the random padding noise.
If an attacker needs 100 requests to guess a character without padding, they might need 1,000-10,000 requests with padding. This is slower but still completely feasible for an automated attack.
What Actually Protects Against BREACH
According to Qualys and security researchers, these mitigations actually work:
1. SameSite Cookies (Most Important)
BREACH requires a cross-site request forgery (CSRF) vector: the attacker must trick the victim’s browser into making requests to the target site with cookies attached. SameSite cookies break this attack vector entirely.
- Chrome, Edge, and Opera now default to
SameSite=Lax - This prevents cross-site POST requests from including cookies
- Most users on these browsers are already protected
However: Firefox and Safari have not adopted this default. Explicitly set SameSite on your cookies:
# In your application or via NGINX headers
add_header Set-Cookie "session=value; SameSite=Lax; Secure; HttpOnly";
2. CSRF Token Randomization Per Request
Modern frameworks like Django and Rails now randomize CSRF tokens on every request. Since the target secret changes constantly, BREACH cannot extract it character by character.
If your application uses static CSRF tokens, update your framework or implement per-request token rotation.
3. Disable Compression for Sensitive Responses
The most bulletproof solution is disabling compression for responses containing both user input and secrets:
location /api/sensitive {
gzip off;
proxy_pass http://backend;
}
This has a bandwidth cost but eliminates the vulnerability entirely.
4. Separate Secrets from User Input
Architect your application so secrets never appear in the same response as user-controlled content:
- Return CSRF tokens via separate API calls
- Use HttpOnly cookies for session identifiers (cookies are not in the response body)
- Deliver sensitive data through JavaScript-only endpoints that don’t reflect user input
The Length Hiding Module: When It Might Still Be Useful
Despite its limitations, the length hiding module is not entirely useless. It provides defense in depth as one layer among many. If you’ve already implemented SameSite cookies and CSRF token rotation, length hiding adds marginal additional protection by increasing the attacker’s workload.
How the Module Works
The module appends a randomly generated HTML comment to response bodies:
<!-- random-length HTML comment: JnSLGWeWYWsoJ4dXS3ubLw3YOu3zfGTotlzx7UJUo26xuXICQ2cbpVy1Dprgv8Icj6QfOZx2Ptp9HxCVoevTxhKzMzV6xeYXao0oCngRWJRb4Tvive1iBAXLzrHlLg6jKwNKXrct0tJuA2TvWIRVIng6UoffIbCQLPbi63PwmWemOxVi6m3CPa6hCbAK2CaBR1jLux7UJa4WNN4H0yIDMElMglWWouY5m5FUqAn0afMmtErj0zkA2LMWxisZRES38XLoYycySmaBrIih5IixUsJFR0ei4uZ0IifgV5SnitoNzMusSQem9npObHuU2HKApneAjwnFdPSQZA9sRdSOE8agDI05P832mV1JIcOjsg0FgzxvSG7UEX0HdqBqp2jPOYYW0k5gGtmkiXWydRJfn9lGomxReUeqq2Aec69gplEM6a8aqH5TFgXrGK8jcaPISQlsKkMxJQ7Fp6fVDbmI59xCIvlk -->
The random string varies from 1 to 2048 bytes (configurable). This forces attackers to make more requests to average out the noise, but does not prevent the attack.
Installation
If you still want to use the module as part of a defense-in-depth strategy, install it from the GetPageSpeed repository. Use release 3 or later of the RPM (or release 10 or later of the DEB), which includes the pass-through fix described in Pre-compressed responses below.
RHEL, CentOS, AlmaLinux, Rocky Linux, Fedora, Amazon Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-length-hiding
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-length-hiding
On Debian and Ubuntu, the package handles module loading automatically, so you can skip the load_module step below.
Loading the module on RPM-based systems
Load the module by adding this line at the top of your /etc/nginx/nginx.conf, before the events block:
load_module modules/ngx_http_length_hiding_filter_module.so;
Verify the module loads correctly:
nginx -t
Upgrading the module
After you update the module package, nginx -s reload (or systemctl reload nginx) is not enough. A reload keeps the old shared object mapped in the master process, so NGINX keeps running the previous module code. Swap in the new binary and module with a graceful binary upgrade, or restart:
sudo service nginx upgrade
We checked this by downgrading the module package and reloading: NGINX kept behaving like the newer build until service nginx upgrade ran.
Configuration Directives
length_hiding
Enables or disables the feature for responses.
Syntax: length_hiding on | off;
Default: off
Context: http, server, location, if in location
length_hiding_max
Sets the maximum length of the random string inside the HTML comment.
Syntax: length_hiding_max size;
Default: 2048
Context: http, server, location
Valid range: 256 to 2048 bytes
length_hiding_types
Specifies which MIME types receive the treatment.
Syntax: length_hiding_types mime-type ...;
Default: text/html
Context: http, server, location
Note: The module appends HTML comments. Applying it to non-HTML formats like JSON may produce technically invalid output.
Configuration Example
server {
listen 443 ssl;
server_name example.com;
# Enable compression - see our gzip guide for details
gzip on;
gzip_types text/html text/css application/javascript;
# Length hiding as defense-in-depth (not primary protection)
length_hiding on;
location / {
root /var/www/html;
}
# Disable compression entirely for highly sensitive endpoints
location /api/tokens {
gzip off;
length_hiding off;
proxy_pass http://backend;
}
}
For optimal compression settings, see our guide on NGINX gzip compression.
Verifying the Module Works
Varying transfer sizes alone prove nothing. A broken setup that appends the padding outside the compressed stream also varies in size, and an attacker can simply ignore bytes that are not part of the compressed data. What you need to confirm is that the comment is inside the compressed body and that the stream is clean. Request the page with Accept-Encoding: gzip, decode it, and look for the comment:
for i in 1 2 3; do
curl -s -H "Accept-Encoding: gzip" https://example.com/protected-page \
| gzip -dc | grep -c 'random-length HTML comment'
done
Each request should print 1. Next, check that nothing follows the compressed stream:
curl -s -H "Accept-Encoding: gzip" https://example.com/protected-page | gzip -dc > /dev/null
A correct setup prints nothing. If the padding sits outside the stream, gzip reports decompression OK, trailing garbage ignored. For Brotli, pipe through brotli -dc instead, with Accept-Encoding: br. For a request without Accept-Encoding, the comment is simply the last thing in the body.
Remember: this padding slows attackers but does not stop them.
Pre-compressed responses are passed through
The module only pads responses that NGINX itself sends uncompressed or compresses at the edge (gzip on, or the Brotli module). The comment is added before compression, so it ends up inside the compressed stream, and that is where it hides the length.
A response that already carries a Content-Encoding header is left untouched. That covers upstream responses that arrive gzip, Brotli, or zstd encoded through proxy_pass, and files served by gzip_static. NGINX cannot add text inside a body it did not compress, and appending the comment after an already-encoded body would corrupt it without hiding anything. Earlier builds did exactly that. Since RPM release 3 (DEB release 10) the module skips these responses, so they reach the client byte for byte as the upstream sent them, with no padding.
If you rely on length hiding for a proxied application, have the backend send uncompressed HTML and let NGINX compress it, or accept that those responses carry no padding. The same applies when the unbrotli module decodes a Brotli upstream: the response is not padded.
Recommended Security Strategy
Here’s the priority order for protecting against BREACH:
| Priority | Mitigation | Effectiveness |
|---|---|---|
| 1 | Set SameSite=Lax or SameSite=Strict on cookies |
Breaks the CSRF attack vector entirely |
| 2 | Rotate CSRF tokens per request | Target changes constantly, cannot be guessed |
| 3 | Separate secrets from user input | No secrets to extract |
| 4 | Disable compression for sensitive responses | Eliminates the vulnerability |
| 5 | Length hiding (this module) | Slows down attacks only |
Do not rely on length hiding as your primary or only BREACH protection.
Troubleshooting
Module Not Loading
If nginx -t fails after adding the load_module directive, verify:
- The module file exists at
/usr/lib64/nginx/modules/ngx_http_length_hiding_filter_module.so - The module version matches your NGINX version
- The
load_moduledirective appears before theeventsblock
Length Hiding Not Working
If responses do not include the random comment:
- Verify
length_hiding on;is set in the correct context - Check that the response MIME type matches
length_hiding_types - Ensure the response is not a HEAD request or 204 No Content
- Check that the response does not already carry a
Content-Encodingheader (a gzip-encoded upstream orgzip_staticfile). Those are passed through unpadded, see Pre-compressed responses - Verify the request is not a subrequest
Alternative Compression Options
While this module works with gzip, you might also consider modern compression algorithms:
- Brotli compression for NGINX offers 15-25% better compression
- Zstd compression for NGINX provides fast decompression
Conclusion
The NGINX length hiding module adds random-length HTML comments to responses, which increases the number of requests an attacker needs to execute a BREACH attack. However, it does not prevent the attack: statistical analysis allows attackers to filter out the random noise.
For real protection against BREACH:
- Set SameSite cookies to break the CSRF vector
- Rotate CSRF tokens per request so the target constantly changes
- Disable compression for responses containing both secrets and user input
Use length hiding only as an additional layer in a defense-in-depth strategy, never as your primary protection.
The module is maintained by Nulab and available under the MIT license. View the source code on GitHub.
Further Reading
- BREACH Attack Official Site
- CVE-2013-3587 Details
- BREACH Attack Wikipedia
- Qualys: Defending Against BREACH
- Current State of BREACH Attack
- nginx Ticket #1977 on Length Hiding
Mitigation plus monitoring. The protections that actually stop BREACH, such as a scoped gzip off, are one-line directives that a config refactor next month can quietly drop. GetPageSpeed Amplify runs scheduled gixy scans across every host and ties findings to live NGINX runtime metrics. Drop-in compatible with the deprecated nginx-amplify-agent (EOL January 2026).
