How an Infinite Loop Vulnerability Happens in Go's Text Normalization and How to Fix It
The fe-tool module was quietly sitting on a time-bomb dependency
The fe-tool/go.mod file lists the dependencies for a Go utility that parses 7-Zip archives (via github.com/bodgit/sevenzip) and Electron ASAR bundles (via github.com/dcboy/go-asar). Both of those libraries lean on golang.org/x/text for Unicode normalization — the process of converting text into a canonical form before comparing, storing, or displaying it. That normalization step is completely invisible to most developers, which is exactly what makes this class of vulnerability so dangerous: the attack surface is buried several layers deep in the dependency tree.
Trivy's dependency scanner flagged golang.org/x/text v0.27.0 in fe-tool/go.mod as matching CVE-2026-56852, a high-severity Denial-of-Service vulnerability. The fix — upgrading to v0.39.0 — is a single line change, but understanding why it matters requires a closer look at what norm.Iter does and how it can be made to spin forever.
The Vulnerability Explained
What is norm.Iter and why does it loop?
golang.org/x/text/unicode/norm provides iterators for walking through Unicode text one normalization segment at a time. The norm.Iter type is designed to be called repeatedly in a loop like this:
// Typical usage pattern (simplified)
var iter norm.Iter
iter.InitString(norm.NFC, inputString)
for !iter.Done() {
segment := iter.Next() // advance to next normalized segment
process(segment)
}
The contract is simple: each call to iter.Next() advances an internal byte offset, and iter.Done() returns true when the offset reaches the end of the input. In affected versions (≤ v0.27.0), a specific class of Unicode byte sequences — containing certain combining characters or malformed code-point boundaries — causes iter.Next() to return without advancing the internal offset. The offset stays at the same position on the next iteration, iter.Done() never becomes true, and the loop runs forever.
The vulnerable dependency line
Before the fix, fe-tool/go.mod pinned:
// fe-tool/go.mod (before fix)
golang.org/x/text v0.27.0 // indirect
Because this is an indirect dependency (pulled in by bodgit/sevenzip and dcboy/go-asar), it would not normally appear on a developer's radar during a routine code review. Yet it is exercised whenever either of those libraries normalizes a filename or string extracted from an archive.
How an attacker could exploit this
fe-tool processes user-supplied archive files. Consider this attack path:
- An attacker crafts a 7-Zip or ASAR archive whose internal filenames contain a malformed Unicode sequence — for example, a combining diacritic that is encoded in a way that prevents
norm.Iterfrom advancing past it. - The victim runs
fe-toolagainst the malicious archive (or a service wrapsfe-tooland processes user-uploaded archives automatically). bodgit/sevenzipcalls intogolang.org/x/textto normalize the filename for path comparison or output.norm.Iter.Next()stalls at the malformed byte offset, the loop never exits, and thefe-toolprocess hangs at 100% CPU until it is killed.
In an automated pipeline — a CI artifact processor, a package registry scanner, a game-mod distribution platform — this single malicious archive could take down the worker indefinitely, constituting a full denial of service.
The Fix
What changed in fe-tool/go.mod
The patch upgrades golang.org/x/text from the vulnerable v0.27.0 to the patched v0.39.0:
- golang.org/x/text v0.27.0 // indirect
+ golang.org/x/text v0.39.0 // indirect
v0.39.0 corrects the loop-exit logic inside norm.Iter so that Next() always advances the byte offset by at least one position, guaranteeing termination regardless of the input content.
Additional changes in the same PR
The diff also promotes github.com/bodgit/sevenzip and github.com/dcboy/go-asar from indirect to direct dependencies:
+require (
+ github.com/bodgit/sevenzip v1.6.1
+ github.com/dcboy/go-asar v0.1.0
+)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
- github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
- github.com/dcboy/go-asar v0.1.0 // indirect
This is a best-practice cleanup: if fe-tool's own code imports these packages directly, they belong in the top-level require block rather than the indirect block. Accurate dependency classification makes it easier for tools like Trivy, govulncheck, and Dependabot to reason about the actual attack surface.
The Go toolchain version was also bumped from go 1.24.5 to go 1.25.0, and fe-tool/go.sum was updated with the new hashes for golang.org/x/text v0.39.0 and the reclassified direct dependencies.
Before vs. after at a glance
| Before | After | |
|---|---|---|
golang.org/x/text version |
v0.27.0 |
v0.39.0 |
norm.Iter infinite-loop risk |
✅ Present | ❌ Fixed |
sevenzip / go-asar classification |
indirect |
direct |
| Go toolchain | 1.24.5 |
1.25.0 |
Prevention & Best Practices
1. Run a vulnerability scanner on every dependency update
Trivy caught this issue precisely because it compares the resolved module versions in go.sum against a curated CVE database. Add it to your CI pipeline:
# GitHub Actions example
- name: Scan dependencies
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
exit-code: '1'
severity: 'HIGH,CRITICAL'
2. Use govulncheck for Go-specific reachability analysis
govulncheck goes further than version matching — it traces whether the vulnerable symbol is actually called in your binary:
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
This can distinguish between a vulnerability that is present in the module graph (as assessed here) and one that is reachable from your code, helping prioritize remediation.
3. Treat indirect dependencies as first-class citizens
The fact that golang.org/x/text was marked // indirect does not reduce its attack surface. Indirect dependencies are compiled into your binary and execute with the same privileges as direct dependencies. Audit them with the same rigor.
4. Apply input length limits before normalization
Even with the patched library, it is good practice to bound the size of any string you pass to a normalization routine:
const maxFilenameBytes = 4096
func safeNormalize(s string) (string, error) {
if len(s) > maxFilenameBytes {
return "", fmt.Errorf("input exceeds maximum allowed length")
}
return norm.NFC.String(s), nil
}
This provides defense-in-depth: even if a future bug resurfaces, an attacker cannot feed an unbounded stream of bytes into the normalizer.
5. Relevant standards
- CWE-835 — Loop with Unreachable Exit Condition
- OWASP A06:2021 — Vulnerable and Outdated Components
- OWASP Dependency Check cheat sheet: enforce automated dependency scanning in CI/CD
Key Takeaways
norm.Iteringolang.org/x/text ≤ v0.27.0is not safe to use with untrusted input — archive filenames, user-uploaded text, or any externally sourced string can trigger the infinite loop.- Indirect dependencies in
go.modcarry real CVEs. The// indirectcomment is a classification hint for the Go toolchain, not a security boundary. - Upgrading from
v0.27.0tov0.39.0is the only reliable fix — input validation alone cannot paper over a loop-exit bug inside the library. - Promoting
bodgit/sevenzipandgo-asarto direct dependencies makes the dependency graph more transparent and ensures future scanners correctly attribute reachability tofe-tool's own code. - A single-line
go.modchange can eliminate a high-severity DoS vector that would otherwise be invisible during normal code review.
How Orbis AppSec Detected This
- Source: User-supplied archive files (7-Zip or ASAR format) processed by
fe-tool, whose internal filenames or string fields may contain arbitrary Unicode byte sequences. - Sink: The
norm.Iter.Next()call path insidegolang.org/x/text v0.27.0, reached transitively throughgithub.com/bodgit/sevenzipandgithub.com/dcboy/go-asarwhen they normalize strings from archive metadata. - Missing control: No upper bound on the version of
golang.org/x/textwas enforced, allowing the vulnerablev0.27.0to remain pinned long after the patch was available; no automated dependency-vulnerability gate was present in the pipeline. - CWE: CWE-835 — Loop with Unreachable Exit Condition
- Fix:
golang.org/x/textwas upgraded fromv0.27.0tov0.39.0infe-tool/go.mod, replacing the defectivenorm.Iterloop-exit logic with a version that always advances the byte offset.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-56852 is a textbook example of how a deeply buried, "invisible" dependency can introduce a high-severity vulnerability into an application that never explicitly calls the affected API. The fe-tool module processes real-world archive files — a classic source of adversarially crafted input — and its transitive dependency on a flawed version of golang.org/x/text meant that any malicious Unicode sequence in an archive filename could hang the process indefinitely.
The fix is surgical and low-risk: a single version bump in go.mod from v0.27.0 to v0.39.0, plus a dependency-graph cleanup that makes the true attack surface explicit. The broader lesson is that dependency hygiene — automated scanning, accurate direct/indirect classification, and CI gates on known CVEs — is not optional overhead. It is the layer of defense that catches the vulnerabilities your code review will never see.