GHSA-x845-2f78-7v36 Blocky DNSSEC validation bypass and validation-cache scope pollution
Blocky DNS proxy bypasses DNSSEC validation and caches forged answers, allowing attackers to poison DNS responses and redirect traffic.
- Exploited in the wild
- Not known
- Login needed
- Not needed
- Attackable
- Over the internet
- Victim action needed
- Not needed
- Fix
- Available
What is GHSA-x845-2f78-7v36?
GHSA-x845-2f78-7v36 is a vulnerability in github.com/0xERR0R/blocky, published on 19 Jun 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.
Official description
Summary
Blocky accepts and caches forged DNS answers while dnssec.validate: true is enabled. The issue has two related exploit paths:
Basic DNSSEC validation bypass. If an untrusted upstream returns an unsigned positive answer for a DNSSEC-signed public domain, Blocky classifies the response as
Insecuresolely because the response contains no RRSIG records. It does not first check the DS/DNSKEY chain to determine whether the queried name is below a signed delegation. The forged unsigned answer is returned and cached.Validation-cache scope pollution through forged insecure proofs. If a response contains some RRSIG material and enters RRset validation, an attacker-controlled response path can still cause Blocky to cache
ValidationResultInsecurefor the bare domain name by returning a DS response with no DS records and an unsigned NSEC/NSEC3 record in the authority section. Blocky treats the mere presence of NSEC/NSEC3 as authenticated DS absence and stores the resultingInsecurestate without validating the parent-zone proof. That cached state is keyed only by domain name and can be reused for later responses and cache hits.
Both paths were reproduced through Blocky's real DNS listener using external UDP DNS client queries. In both reproductions, the malicious upstream was shut down before the second query; Blocky still returned the poisoned answer from its own cache.
DNSSEC validation Configuration
The PoCs use Blocky's documented DNSSEC configuration model. This is not a misconfiguration.
Blocky's own documentation states that the basic DNSSEC configuration is:
dnssec:
validate: true
The documentation says this enables DNSSEC validation with default settings and built-in root trust anchors, and that Blocky will validate DNSSEC-signed domains. It also states that, when DNSSEC validation is enabled, Blocky will:
- set the DNSSEC OK bit on upstream queries;
- validate RRSIG records;
- verify the chain of trust from the root zone to the queried domain using DNSKEY and DS records;
- return SERVFAIL for bogus signatures;
- protect against cache poisoning, man-in-the-middle attacks, DNS spoofing, and forged denial-of-existence.
The implementation-side defaults match this documented usage:
config/dnssec.godefinesDNSSEC.Validateas thednssec.validateoption.config/dnssec.godocumentsTrustAnchors []stringas custom trust anchors; an empty value uses built-in IANA root trust anchors.- The PoCs set
cfg.DNSSEC.Validate = trueand do not overrideTrustAnchors, so they use the documented built-in root trust-anchor path. - The PoCs use
config.NetProtocolTcpUdpas the upstream transport, which is one of the documented upstream protocols. - The cache configuration is normal Blocky behavior:
caching.maxTime >= 0enables caching, and the PoCs set a positivemaxTimeonly to make cache replay observable.
Therefore, the expected behavior for a signed public domain such as cloudflare.com. is not to accept an unsigned forged answer. A validating resolver must determine whether the name is covered by a signed delegation before treating missing signatures as Insecure.
Threat models and attack paths
Attack model 1: untrusted recursive upstream or upstream-path attacker
This is the direct DNSSEC threat model. DNSSEC validation is supposed to protect clients even when the recursive upstream response path is malicious, compromised, or tampered with.
The attacker can be:
- a malicious recursive upstream configured in Blocky;
- an attacker who can tamper with plaintext UDP/TCP DNS traffic between Blocky and its upstream;
- a compromised upstream resolver;
- a misrouted or attacker-controlled conditional upstream.
Attack steps:
- The client queries Blocky for a DNSSEC-signed public name, for example
cloudflare.com. A. - The attacker-controlled upstream returns an unsigned forged positive answer, for example
cloudflare.com. 120 IN A 203.0.113.77. - Blocky observes that the response contains no RRSIG records.
- Blocky returns
ValidationResultInsecurewithout issuing target DS or DNSKEY queries. - The forged answer is returned to the client and cached.
- Later clients receive the cached forged answer, even if the malicious upstream is no longer reachable.
This path is demonstrated by attachments/external-dnssec-basic-bypass/main.go.
Attack model 2: forged insecure proof / validation-cache scope pollution
This path exercises the validator's insecure-proof and cache-scope logic. It is relevant when the response enters RRset validation and when different DNS views or response paths can seed DNSSEC state for the same domain name.
The attacker can be:
- an attacker-controlled recursive upstream;
- a network attacker who can tamper with DS/DNSKEY auxiliary queries;
- a conditional-forwarding or split-horizon configuration that causes the final answer and DNSSEC auxiliary lookups to come from different views;
- a malicious upstream group selected for DNSSEC auxiliary queries but not necessarily for the original user-facing answer.
Attack steps:
- The client queries Blocky for
victim.signed.example. A. - The attacker returns a poisoned A RR and an unrelated decoy RRSIG. The A RRset itself has no matching RRSIG, but the response contains some RRSIG material, so Blocky enters RRset validation instead of the simple
no RRSIGbranch. - Blocky attempts to determine whether
victim.signed.example.is in a signed or unsigned zone by querying DS records. - The attacker returns a DS response with no DS records and an unsigned NSEC record in the authority section.
- Blocky treats the mere presence of NSEC as authenticated DS absence, caches
ValidationResultInsecurefor the bare domain name, and accepts the unsigned A RRset. - The poisoned answer is returned and cached.
- On later queries, Blocky reuses both the poisoned DNS response cache entry and the polluted validation status. The PoC confirms replay after the malicious upstream is shut down.
This path is demonstrated by attachments/external-dnssec-cache-scope-pollution/main.go.
Details
1. no RRSIG is treated as Insecure before chain status is checked
In resolver/dnssec/validator.go, ValidateResponse dispatches as follows:
switch {
case !v.hasAnySignatures(response):
v.logger.Debugf("No RRSIG records found for %s - zone is unsigned", question.Name)
result = ValidationResultInsecure
case len(response.Answer) > 0:
result = v.validateAnswer(ctx, response, question)
...
}
The bug is the assumption that a response with no RRSIG records means the zone is unsigned. That assumption is not valid for a validating resolver. The resolver must first prove that the queried name is below an insecure delegation. For a signed domain, an unsigned positive answer should be Bogus, not Insecure.
The basic bypass PoC uses cloudflare.com., a public DNSSEC-signed domain. Blocky returns NOERROR and the forged A record while issuing zero target DS and DNSKEY queries.
2. Cache writes happen before outer DNSSEC validation can reject or transform the response
server/server.go:526-543 constructs the resolver chain with dnssecResolver before cachingResolver and includes a comment saying DNSSEC validation happens before caching:
dnssecResolver, // DNSSEC validation BEFORE caching - validates all responses before they are cached
cachingResolver,
...
upstreamTree,
However, chained resolver execution is outer-to-inner. DNSSECResolver.Resolve first calls r.next.Resolve, and CachingResolver.Resolve writes cache entries on misses before control returns to the DNSSEC layer:
resolver/dnssec_resolver.go:88-96: the DNSSEC resolver callsr.next.Resolve(ctx, request)beforeValidateResponse.resolver/caching_resolver.go:225-230: on cache miss, the cache resolver calls the next resolver and then immediately callsputInCache.- `resolver/caching_resolver.
Which versions are affected by GHSA-x845-2f78-7v36?
3 open source packages ship an affected version, including github.com/0xERR0R/blocky.
| Package | Affected | Fixed in |
|---|---|---|
github.com/0xERR0R/blocky Go | From 0.28.0 | 0.32.0 |
github.com/0xERR0R/blocky Go | From 0.28.0 | 0.32.0 |
github.com/0xERR0R/blocky Go | From 0.28.0 | 0.32.0 |
How do I fix GHSA-x845-2f78-7v36?
Upgrade github.com/0xERR0R/blocky to 0.32.0 or later.
Is GHSA-x845-2f78-7v36 being exploited?
There is no known exploitation of GHSA-x845-2f78-7v36 in the wild and no public exploit code.
Sources
Also known as GO-2026-5745.