GHSA-x845-2f78-7v36 Blocky DNSSEC validation bypass and validation-cache scope pollution

Not scored Published

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:

  1. Basic DNSSEC validation bypass. If an untrusted upstream returns an unsigned positive answer for a DNSSEC-signed public domain, Blocky classifies the response as Insecure solely 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.

  2. 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 ValidationResultInsecure for 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 resulting Insecure state 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.go defines DNSSEC.Validate as the dnssec.validate option.
  • config/dnssec.go documents TrustAnchors []string as custom trust anchors; an empty value uses built-in IANA root trust anchors.
  • The PoCs set cfg.DNSSEC.Validate = true and do not override TrustAnchors, so they use the documented built-in root trust-anchor path.
  • The PoCs use config.NetProtocolTcpUdp as the upstream transport, which is one of the documented upstream protocols.
  • The cache configuration is normal Blocky behavior: caching.maxTime >= 0 enables caching, and the PoCs set a positive maxTime only 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:

  1. The client queries Blocky for a DNSSEC-signed public name, for example cloudflare.com. A.
  2. The attacker-controlled upstream returns an unsigned forged positive answer, for example cloudflare.com. 120 IN A 203.0.113.77.
  3. Blocky observes that the response contains no RRSIG records.
  4. Blocky returns ValidationResultInsecure without issuing target DS or DNSKEY queries.
  5. The forged answer is returned to the client and cached.
  6. 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:

  1. The client queries Blocky for victim.signed.example. A.
  2. 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 RRSIG branch.
  3. Blocky attempts to determine whether victim.signed.example. is in a signed or unsigned zone by querying DS records.
  4. The attacker returns a DS response with no DS records and an unsigned NSEC record in the authority section.
  5. Blocky treats the mere presence of NSEC as authenticated DS absence, caches ValidationResultInsecure for the bare domain name, and accepts the unsigned A RRset.
  6. The poisoned answer is returned and cached.
  7. 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 calls r.next.Resolve(ctx, request) before ValidateResponse.
  • resolver/caching_resolver.go:225-230: on cache miss, the cache resolver calls the next resolver and then immediately calls putInCache.
  • `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.

PackageAffectedFixed in
github.com/0xERR0R/blocky GoFrom 0.28.00.32.0
github.com/0xERR0R/blocky GoFrom 0.28.00.32.0
github.com/0xERR0R/blocky GoFrom 0.28.00.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.

Is GHSA-x845-2f78-7v36 in your software?

Upload an SBOM and sbomify checks it against every vulnerability here, every day.

Check your SBOMs for free