The Fastly VCL == false Trap
How using == false instead of ! in Fastly VCL compound conditions can silently break your logic
Laurent Goudet · February 10, 2026 · 7 min read
The Bug
A Fastly VCL condition with two == false checks in an
&& chain was silently evaluating to true,
bypassing a domain filter. The condition was supposed to restrict logic to a
specific domain, but it was being applied to all traffic.
The engineer spent hours debugging, isolating each sub-condition individually. Each condition worked correctly on its own. The bug only manifested when
two or more == false comparisons
were chained together with &&.
Broken
if (req.http.X-Bot == false
&& var.is_blocked == false
&& req.http.host ~ “example.com”) {
// Domain-specific logic
// BUG: always entered!
}Fixed
if (req.http.X-Bot == false
&& !var.is_blocked
&& req.http.host ~ “example.com”) {
// Domain-specific logic
// Works correctly
}The Investigation
We dug into Fastly’s VCL documentation looking for an explanation. What we found was surprising:
Fastly’s operator precedence table only lists four operators
: (), !, &&, and ||.
Comparison operators like ==, !=, ~,
and > are documented in a separate section, without any
defined precedence level.
In contrast, the open-source Varnish VCL parser (which Fastly’s VCL is
derived from) has a clear recursive descent hierarchy where ==
binds tighter than &&. The omission from Fastly’s documentation
raised the question: does Fastly’s fork handle operator precedence
differently?
Fastly’s official VCL reference documents operator precedence as a
four-level table: () → ! → && →
||. Comparison operators (==, !=,
~, >, <, etc.) are
absent from this table entirely.
In Varnish’s open-source parser (vcc_expr.c), the recursive
descent functions follow a strict hierarchy: vcc_expr_cmp
(comparisons) is called from vcc_expr_not, which means
== naturally has higher precedence than &&. If
Fastly’s fork deviates from this hierarchy, a == false && b
could be misparsed as a == (false && b).
Parse Tree Comparison
Expected
Actual (Fastly)
| Aspect | Varnish (open-source) | Fastly VCL |
|---|---|---|
| Parser type | Recursive descent (vcc_expr.c) | Proprietary fork |
| == precedence vs && | ✓ Higher (well-defined) | ✗ Undocumented |
| Precedence table | Implicit in parser hierarchy | Only lists () ! && || |
| Cross-type == | Strict type checking | “Equality defined for all types” |
Truth Table: Expected vs. Actual Results
| X-Bot | is_blocked | Expected | Actual |
|---|---|---|---|
| false | false | true | true |
| false | true | false | true✗ |
| true | false | false | true✗ |
| true | true | false | true✗ |
The Proof
We deployed a dedicated PoC on a live Fastly service to confirm the bug. The
test endpoint runs four variations of the same && chain where
all three conditions should evaluate to false — the third
condition checks
req.http.Host == “www.nonexistent-domain.example”, which can
never match. None of the blocks should execute.
$ curl -sD- https://www.syd1.fln-dev.net/_vcl-bug-test{“\n”}
x-test1-both-eqfalse:
REACHED-BUG
x-test2-both-bang: not-reached
x-test3-mixed: not-reached
x-test4-parens: not-reached
The four tests and their results:
Test | Pattern | Result |
|---|---|---|
1 | STRING == false && BOOL == false | REACHED-BUG |
2 | !STRING && !BOOL | not-reached |
3 | STRING == false && !BOOL | not-reached |
4 | (STRING == false) && (BOOL == false) | not-reached |
Test 1 confirms the bug: two == false comparisons in a
&& chain cause the entire condition to evaluate to
true, regardless of subsequent conditions. Tests 2-4 show three
independent workarounds: using ! for both (Test 2), mixing
== false with ! (Test 3), or wrapping each
comparison in explicit parentheses (Test 4).
If == and && share the same precedence and bind
left-to-right, the expression
A == false && B == false && (C == D) parses as
((((A == false) && B) == false) && (C == D)). Walking through
the evaluation: A == false → true, true && B →
false, false == false → true — but then
true && (C == D) → false. The final && should
bring it back to false. Yet the PoC shows it evaluates to true.
This points to a short-circuit code generation issue on
top of the precedence problem. Compilers commonly optimize
X == false in a && chain by inverting the branch
condition — instead of computing the comparison and branching on the
result, they evaluate X directly and flip the jump polarity.
With two == false in the same chain, the second inversion may
corrupt the jump target, causing the remaining conditions to be skipped
entirely and execution to fall through to the body. This is consistent
with all four PoC results: the bug requires exactly two
== false in the chain (Test 1), a single one works fine (Test
3), ! uses a different code path entirely (Test 2), and
explicit parentheses force the comparisons to be pre-evaluated before
entering the short-circuit chain (Test 4).
HTTP headers in VCL are strings. Writing
req.http.X-Bot == false compares a STRING to
a BOOL. Fastly’s documentation states “equality is
defined for all types” but does not specify what happens when the types
differ.
Does “0” == false evaluate to true? Does
"" == false? What about a missing header? Varnish’s type
system is strict, but Fastly’s fork may handle cross-type comparisons with
implicit coercion. This undocumented behavior, combined with the
precedence issue, creates a minefield for compound conditions.
The Fix
Replacing just one of the two == false with ! was
enough to fix the bug. The final condition uses !var.is_blocked
instead of var.is_blocked == false, while keeping the HTTP
header comparison as req.http.X-Bot == false.
Explicit parentheses around each comparison also work:
(req.http.X-Bot == false) && (var.is_blocked == false) forces
the parser to group the comparisons correctly, preventing the precedence
issue.
Note: The “fixed” code still uses
req.http.X-Bot == false, which the Key Takeaways below flag as
bad practice (comparing a string header to a boolean). This was the minimal
hotfix — replacing one == false to break the compound
precedence issue. The recommended practice is to use !
everywhere: !req.http.X-Bot && !var.is_blocked.
Use ! instead of == false.
The negation operator has well-defined precedence in Fastly’s documentation. The equality operator does not.
Don’t compare HTTP headers to booleans. Headers are
strings. Use !req.http.X-Bot (tests for existence/truthiness)
rather than req.http.X-Bot == false.
Be extra cautious with compound conditions. The bug only
manifested with two == false in a chain. Single uses appeared
to work fine, making this extremely hard to catch in testing.
Confirmed with a live PoC on Fastly’s VCL platform in February 2026. The bug
has been reported to Fastly support. The likely root cause is undefined
operator precedence for == combined with a short-circuit code
generation issue when multiple == false comparisons appear in the
same && chain. Exact confirmation pending from Fastly.
Frequently Asked Questions
Why does == false break in Fastly VCL compound conditions?
Fastly's VCL documentation doesn't define precedence for the == operator relative to &&. When two == false comparisons are chained with &&, the condition may be misparsed.
How do you negate a condition in Fastly VCL safely?
Use the ! operator instead of == false. The ! operator has well-defined precedence in Fastly's documentation, while == does not.
Can you compare HTTP headers to booleans in VCL?
Headers are strings in VCL. Comparing a string to a boolean (req.http.X-Bot == false) relies on undocumented cross-type coercion. Use !req.http.X-Bot instead.
Other deep-dives
TLS vs mTLS Handshake
Comparing standard and mutual TLS authentication flows
Network SecurityTLS 1.2 vs TLS 1.3 Handshake
Comparing handshake efficiency and security improvements
Network EngineeringIPv6-Only Network with NAT64/464XLAT
Running an IPv6-only local network while maintaining IPv4 internet connectivity
AI & IndustrySomething Big Is Happening — But It's Not What You Think
Why AI is an abstraction layer shift, not an extinction event — a practitioner's response to the viral AI essay
AI & IndustryAI Agent Orchestration at Scale — What Actually Works in Production
Patterns and hard lessons from running multi-agent systems at 80M+ user scale: routing, fallback chains, context management, and why most agent architectures fail.
Network SecurityDNSSEC: Chain of Trust from Root to This Domain
How DNSSEC builds a cryptographic chain of trust from the DNS root to this zone — with Pulumi setup and live dig verification
Network SecurityRolling Out DMARC Enforcement at Scale
A practical guide to deploying DMARC across a large platform — SPF, DKIM, and alignment fixes across AWS SES, Google Workspace, Postfix relays, and dozens of domains
Cloud SecurityYour Google Maps API Key Can Now Drain Your Bank Account
Google silently changed API key permissions so that keys meant for Maps can now call Gemini AI. Here's how to audit your GCP projects and lock down exposed keys before someone else finds them.