GHSA-X845-2F78-7V36 is a high-severity security vulnerability in github.com/0xERR0R/blocky (go), affecting versions >= 0.28.0, < 0.32.0. It is fixed in 0.32.0.
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 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. 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: 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: 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 ValidationResultInsecure without 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 RRSIG branch. 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 ValidationResultInsecure for 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 no RRSIG is treated as Insecure before chain status is checked In resolver/dnssec/validator.go, ValidateResponse dispatches as follows: 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. 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: 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/dnssecresolver.go:88-96: the DNSSEC resolver calls r.next.Resolve(ctx, request) before ValidateResponse. resolver/cachingresolver.go:225-230: on cache miss, the cache resolver calls the next resolver and then immediately calls putInCache. resolver/cachingresolver.go:326-341: the cache write only checks rcode and basic cacheability; it does not bind the entry to a DNSSEC validation result. The practical result is that the DNS response cache can store data that has not yet survived final DNSSEC validation. Validation cache is keyed only by bare domain name resolver/dnssec/chain.go:16-31 exposes: resolver/dnssec/validator.go:638-642 reuses this cache for zone-security checks: The key does not include: qclass; qtype or proof purpose; current client view; ECS, client IP, client name, or request client ID; conditional-forwarding branch; effective upstream group; proof source zone; parent zone; trust-anchor path; validation policy or algorithm set. This allows one response path or proof purpose to seed a DNSSEC status for another path. Unsigned NSEC/NSEC3 presence is treated as authenticated DS absence resolver/dnssec/validator.go:655-667 queries DS records when an RRset has no matching RRSIG. If no DS records are extracted, it calls handleNoDSRecords. resolver/dnssec/validator.go:682-690 then does: This code does not validate the NSEC/NSEC3 RRset signature and does not validate the parent zone chain before trusting the denial proof. The comment calls this an authenticated denial of DS existence, but the code only checks for record presence. DNSSEC auxiliary queries do not preserve original request context resolver/dnssecresolver.go:47-52 creates the validator with upstream as the resolver used for DS/DNSKEY lookups. resolver/dnssec/query.go:57-69 builds synthetic requests containing only qname/qtype and sends them to v.upstream.Resolve. Those synthetic requests do not preserve the original request's client IP, client names, ECS data, request client ID, or conditional-forwarding context. resolver/upstreamtreeresolver.go:123-162 chooses upstream groups based on client metadata; missing metadata can cause DNSSEC auxiliary queries to use a different upstream view from the answer being validated. This is a scope problem even apart from the direct basic bypass. Reproduction Environment Repository: /home/hurrison/workspace/dnssec/repos/blocky Commit: e0ea9b3ea56e3d074569abd3010251e7c6ebd593 No root privileges required. No public DNS dependency; PoCs use local loopback high ports. Both PoCs query Blocky's real DNS listener through UDP. PoC 1: basic unsigned-response DNSSEC bypass Run: Artifact: Key output: Interpretation: cloudflare.com. is treated as if it were insecure only because the forged response contained no RRSIG records. Blocky does not query DS or DNSKEY for the target before accepting the answer. The second answer is served after the malicious upstream is shut down, proving cache replay. PoC 2: forged insecure proof and validation-cache scope pollution Run: Artifact: Key output: Interpretation: The response contains an unrelated RRSIG to force the RRset-validation path. The A RRset has no matching RRSIG. The forged DS response contains no DS and an unsigned NSEC in authority. Blocky caches Insecure for the domain and returns the poisoned answer. The second response is served after the malicious upstream is shut down. Expected behavior For a DNSSEC validating resolver: Missing RRSIGs in a positive response must not automatically imply an insecure zone. The resolver must prove that the queried name is under an insecure delegation before accepting an unsigned answer. If the parent chain indicates the name should be signed, an unsigned positive answer must be treated as bogus. DS absence must be proven by authenticated denial of existence, not by the mere presence of NSEC/NSEC3 records. DNS response cache entries must not be written before the final DNSSEC decision, or they must be bound to validation metadata that is checked on cache hit. Validation cache entries must be scoped to the proof purpose, class, view, upstream group, proof source, and trust path. Actual behavior Unsigned positive responses are classified as Insecure without DS/DNSKEY chain checks. CachingResolver writes responses before outer DNSSEC validation runs. An unsigned NSEC/NSEC3 in a DS response can mark a domain as Insecure. The Insecure result is cached by bare domain name. Poisoned answers are replayed from Blocky's DNS cache on later external queries. Impact The impact is DNSSEC validation bypass and persistent DNS cache poisoning: forged A/AAAA/CNAME/MX/TXT records can be returned for signed domains; poisoned records can be replayed to later clients for the cache TTL; traffic can be redirected to attacker-controlled infrastructure; update systems, package mirrors, service discovery, mail routing, and TLS bootstrapping flows may be affected depending on client behavior; split-horizon and conditional-forwarding deployments may suffer cross-view validation-state pollution; an attacker can also induce false Bogus or Indeterminate states in related logic, causing targeted SERVFAIL or AD-bit stripping. The basic bypass is sufficient for a malicious or intercepted recursive upstream to defeat Blocky's documented DNSSEC protection. The cache-scope pollution path shows additional design risk in deployments with multiple views, upstream groups, or conditional forwarding. Attachments blocky-dnssec-validation-cache-scope-pollution-attachments.zip Attachment descriptions: attachments/external-dnssec-basic-bypass/main.go: external PoC for the direct unsigned-response DNSSEC bypass. attachments/external-dnssec-basic-bypass/README.md: usage notes for the basic bypass PoC. attachments/external-dnssec-cache-scope-pollution/main.go: external PoC for forged insecure proof and validation-cache scope pollution. attachments/external-dnssec-cache-scope-pollution/README.md: usage notes for the cache-scope PoC. artifacts/basic-bypass-output.txt: recorded output for PoC 1. artifacts/poc-output.txt: recorded output for PoC 2. Credit Yuheng Zhang @ Tsinghua University Jianjun Chen@ Tsinghua University
GHSA-X845-2F78-7V36 has a CVSS score of 8.6 (High). The vector is network-reachable, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment.
A fixed version is available (0.32.0). Upgrading removes the vulnerable code path.
go
github.com/0xERR0R/blocky (>= 0.28.0, < 0.32.0)github.com/0xERR0R/blocky → 0.32.0 (go)Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter instead of chasing every advisory.
Kodem's runtime-powered SCA identifies whether GHSA-X845-2F78-7V36 is reachable in your applications. Explore open-source security for your team.
See if GHSA-X845-2F78-7V36 is reachable in your applications. Get a demo
Already deployed Kodem? See GHSA-X845-2F78-7V36 in your environment →Upgrade github.com/0xERR0R/blocky to 0.32.0 or later to resolve this vulnerability.
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
GHSA-X845-2F78-7V36 is a high-severity security vulnerability in github.com/0xERR0R/blocky (go), affecting versions >= 0.28.0, < 0.32.0. It is fixed in 0.32.0.
GHSA-X845-2F78-7V36 has a CVSS score of 8.6 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
github.com/0xERR0R/blocky (go) versions >= 0.28.0, < 0.32.0 is affected.
Yes. GHSA-X845-2F78-7V36 is fixed in 0.32.0. Upgrade to this version or later.
Whether GHSA-X845-2F78-7V36 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
Upgrade github.com/0xERR0R/blocky to 0.32.0 or later.