# 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.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not needed
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 19 Jun 2026
- **Also known as:** GO-2026-5745

## 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:

```yaml
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:

```go
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:

```text
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.


| Package | Ecosystem | 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.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-x845-2f78-7v36 and fix it if it is affected.

What it is: 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.
Exploitation: There is no known exploitation of GHSA-x845-2f78-7v36 in the wild and no public exploit code.

Affected packages and fixed versions:
- github.com/0xERR0R/blocky (Go): affected from 0.28.0; fixed in 0.32.0
- github.com/0xERR0R/blocky (Go): affected from 0.28.0; fixed in 0.32.0
- github.com/0xERR0R/blocky (Go): affected from 0.28.0; fixed in 0.32.0

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-x845-2f78-7v36/ (Markdown: https://insights.sbomify.com/GHSA-x845-2f78-7v36/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-x845-2f78-7v36

---
From sbomify Insights: https://insights.sbomify.com/GHSA-x845-2f78-7v36/
Summaries are generated and may contain mistakes; the official description above is the record.
