[ 
https://issues.apache.org/jira/browse/WSS-727?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Ivan Novak updated WSS-727:
---------------------------
    Description: 
Verifying a SOAP message with a signed SwA attachment requires roughly *2.3x 
the attachment size* in heap, regardless of whether the attachment is backed by 
disk and regardless of whether the caller supplies a properly streaming, 
mark-capable source stream. Signing the same message is streaming and needs a 
constant ~20 MB.

The cause is {{SignatureProcessor.verifyXMLSignature()}} (ws-security-dom, line 
353):
{code:java}
XMLValidateContext context = new DOMValidateContext(key, elem);
context.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
{code}
In Santuario that single property controls two unrelated caches 
({{{}DOMReference.transform(){}}}, xmlsec 4.0.4, DOMReference.java:474-476 and 
:566):
 * the dereferenced {{Data}} - cheap: for a {{cid:}} URI the resolver returns a 
zero-length input, and it is the half WSS4J actually consumes 
({{{}buildProtectedRefs(){}}} uses it to work out what each Reference covered, 
and for an attachment to synthesise the {{<attachment>}} marker element and set 
{{{}WSDataRef.setAttachment(true){}}});
 * the pre-digested input - {{DigesterOutputStream}} retains every octet fed to 
the digest in an {{UnsyncByteArrayOutputStream}} that grows by doubling, plus 
one more full array copy in {{{}getInputStream(){}}}.

For an SwA reference the last transform 
({{{}AttachmentContentSignatureTransform{}}}) writes the attachment straight 
into that stream, so a 200 MB attachment costs a 256 MB backing array plus a 
200 MB copy - ~456 MB live at once. The buffered copy is dead weight: its only 
consumer is {{{}Reference.getDigestInputStream(){}}}, which nothing in WSS4J 
calls.
h3. Reproduction

Attached: {{LargeAttachmentRepro.java}} (self-contained, no test-jar 
dependencies).
{code:java}
java -Xmx<N> -cp <ws-security-dom test classpath> LargeAttachmentRepro 
sign|verify 200 <workdir>
{code}
It signs / verifies a 200 MB {{application/octet-stream}} attachment read from 
a file. The attachment's source stream implements 
{{{}mark(){}}}/{{{}reset(){}}} by re-opening the file, so it contributes zero 
heap - any growth observed is the digest-side cache alone.

Environment: WSS4J 4.0.1, xmlsec 4.0.4, Temurin JDK 26.0.1, Linux.
||scenario||-Xmx||result||
|sign, 200 MB attachment|256m|OK, peak used heap 22 MB|
|verify, 200 MB attachment|256m|OutOfMemoryError|
|verify, 200 MB attachment|400m|OutOfMemoryError (peak 394 MB)|
|verify, 200 MB attachment|512m|OK, peak used heap *466 MB*|
|verify, with attached patch|256m|OK, peak used heap *14 MB*|
{code:java}
java.lang.OutOfMemoryError: Java heap space
        at 
org.apache.xml.security.utils.UnsyncByteArrayOutputStream.expandSize(UnsyncByteArrayOutputStream.java:113)
        at 
org.apache.xml.security.utils.UnsyncByteArrayOutputStream.write(UnsyncByteArrayOutputStream.java:63)
        at 
org.apache.jcp.xml.dsig.internal.DigesterOutputStream.write(DigesterOutputStream.java:83)
        at 
org.apache.xml.security.utils.UnsyncBufferedOutputStream.write(UnsyncBufferedOutputStream.java:52)
        at 
org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.processAttachment(AttachmentContentSignatureTransform.java:218)
        at 
org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.transform(AttachmentContentSignatureTransform.java:122)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMTransform.transform(DOMTransform.java:170)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMReference.transform(DOMReference.java:488)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMReference.validate(DOMReference.java:415)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMXMLSignature.validate(DOMXMLSignature.java:295)
        at 
org.apache.wss4j.dom.processor.SignatureProcessor.verifyXMLSignature(SignatureProcessor.java:381)
        ...
{code}
h3. Proposed fix (attached: {{wss4j-patch.diff}} - WSS4J-only, no Santuario 
change required)

The outer {{xmlSignature.validate(context)}} is replaced by the equivalent 
explicit loop - {{SignatureValue}} check plus per-Reference 
{{Reference.validate()}} - with {{cacheReference}} toggled off for SwA 
attachment References. This is behaviour-preserving: the property is read per 
Reference at transform time, {{Reference.validate()}} is public JSR-105 and 
runs at most once per Reference, short-circuit semantics are kept, and 
{{DOMXMLSignature.validate()}} does nothing else WSS4J relies on (Manifest 
validation is gated on {{{}org.jcp.xml.dsig.validateManifests{}}}, which WSS4J 
never sets - and Manifests are rejected by {{checkBSPCompliance}} anyway).

The second half of the patch is required, not optional: with caching off, 
{{getDereferencedData()}} returns null, so {{buildProtectedRefs()}} must 
recognise attachment References by their Transform algorithm instead - toggling 
the flag without it makes every signed attachment fail verification with 
{{{}FAILED_CHECK{}}}. The resulting {{WSDataRef}} is byte-for-byte what the old 
code produced (same synthesised {{<attachment>}} element, same 
{{{}setAttachment(true){}}}).

Non-attachment References keep {{{}cacheReference=true{}}}, so element 
recovery, the STR-dereference path (WSS-222) and the anti-wrapping checks that 
depend on it are untouched. The Santuario {{Reference}} objects never escape 
{{{}SignatureProcessor{}}}, and ws-security-stax does not use the property.

Test results with the patch applied: {{mvn -pl ws-security-dom test}} - 563 
tests, 0 failures, 0 errors, 5 skipped (unchanged from baseline), including 
{{AttachmentTest}} (17), {{XOPAttachmentTest}} (13) and both negative 
attachment-signature tests; {{mvn -pl integration test}} - 15 tests, 0 
failures. The 200 MB verify above passes under {{-Xmx256m}} with a 14 MB peak.
h3. Notes
 * This is WSS4J's defect, not Santuario's: the {{XMLValidateContext}} javadoc 
specifies {{cacheReference}} as one switch for both caches, so {{DOMReference}} 
cannot drop the digest-input half without breaking its contract. WSS4J asks for 
the full cache and then consumes only half of it. (Decoupling the caches 
upstream fixes the symptom too, but would have to be a new opt-in property plus 
a Santuario release; the WSS4J patch needs neither.)
 * Distinct from WSS-638, which is about {{processAttachment()}} buffering a 
non-mark-capable *source* stream via 
{{BufferedInputStream.mark(Integer.MAX_VALUE)}} and is still reproducible here 
on both the sign and verify paths. That one has a caller-side workaround (a 
disk-backed, mark-capable stream); this one has none, because the digest-side 
cache retains the attachment independently of the source stream. Both need 
fixing for large attachments to verify in bounded memory.
 * The {{cacheReference}} call has been present since the {{org.apache.wss4j}} 
rename (WSS4J 2.0, commit f647a91bd), so 2.x and 3.x are affected too; measured 
on 4.0.1 only.

  was:
[^LargeAttachmentRepro.java]Verifying a SOAP message with a signed SwA 
attachment requires roughly *2.3x the attachment size* in heap, regardless of 
whether the attachment is backed by disk and regardless of whether the caller 
supplies a properly streaming, mark-capable source stream. Signing the same 
message is streaming and needs a constant ~20 MB.

The cause is {{SignatureProcessor.verifyXMLSignature()}} (ws-security-dom, line 
353):
{code:java}
XMLValidateContext context = new DOMValidateContext(key, elem);
context.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
{code}
In Santuario that single property controls two unrelated caches 
({{{}DOMReference.transform(){}}}, xmlsec 4.0.4, DOMReference.java:474-476 and 
:566):
 * the dereferenced {{Data}} - cheap: for a {{cid:}} URI the resolver returns a 
zero-length input, and it is the half WSS4J actually consumes 
({{{}buildProtectedRefs(){}}} uses it to work out what each Reference covered, 
and for an attachment to synthesise the {{<attachment>}} marker element and set 
{{{}WSDataRef.setAttachment(true){}}});
 * the pre-digested input - {{DigesterOutputStream}} retains every octet fed to 
the digest in an {{UnsyncByteArrayOutputStream}} that grows by doubling, plus 
one more full array copy in {{{}getInputStream(){}}}.

For an SwA reference the last transform 
({{{}AttachmentContentSignatureTransform{}}}) writes the attachment straight 
into that stream, so a 200 MB attachment costs a 256 MB backing array plus a 
200 MB copy - ~456 MB live at once. The buffered copy is dead weight: its only 
consumer is {{{}Reference.getDigestInputStream(){}}}, which nothing in WSS4J 
calls.
h3. Reproduction

Attached: {{LargeAttachmentRepro.java}} (self-contained, no test-jar 
dependencies).
{code:java}
java -Xmx<N> -cp <ws-security-dom test classpath> LargeAttachmentRepro 
sign|verify 200 <workdir>
{code}
It signs / verifies a 200 MB {{application/octet-stream}} attachment read from 
a file. The attachment's source stream implements 
{{{}mark(){}}}/{{{}reset(){}}} by re-opening the file, so it contributes zero 
heap - any growth observed is the digest-side cache alone.

Environment: WSS4J 4.0.1, xmlsec 4.0.4, Temurin JDK 26.0.1, Linux.
||scenario||-Xmx||result||
|sign, 200 MB attachment|256m|OK, peak used heap 22 MB|
|verify, 200 MB attachment|256m|OutOfMemoryError|
|verify, 200 MB attachment|400m|OutOfMemoryError (peak 394 MB)|
|verify, 200 MB attachment|512m|OK, peak used heap *466 MB*|
|verify, with attached patch|256m|OK, peak used heap *14 MB*|
{code:java}
java.lang.OutOfMemoryError: Java heap space
        at 
org.apache.xml.security.utils.UnsyncByteArrayOutputStream.expandSize(UnsyncByteArrayOutputStream.java:113)
        at 
org.apache.xml.security.utils.UnsyncByteArrayOutputStream.write(UnsyncByteArrayOutputStream.java:63)
        at 
org.apache.jcp.xml.dsig.internal.DigesterOutputStream.write(DigesterOutputStream.java:83)
        at 
org.apache.xml.security.utils.UnsyncBufferedOutputStream.write(UnsyncBufferedOutputStream.java:52)
        at 
org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.processAttachment(AttachmentContentSignatureTransform.java:218)
        at 
org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.transform(AttachmentContentSignatureTransform.java:122)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMTransform.transform(DOMTransform.java:170)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMReference.transform(DOMReference.java:488)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMReference.validate(DOMReference.java:415)
        at 
org.apache.jcp.xml.dsig.internal.dom.DOMXMLSignature.validate(DOMXMLSignature.java:295)
        at 
org.apache.wss4j.dom.processor.SignatureProcessor.verifyXMLSignature(SignatureProcessor.java:381)
        ...
{code}
h3. Proposed fix (attached: {{wss4j-patch.diff}} - WSS4J-only, no Santuario 
change required)

The outer {{xmlSignature.validate(context)}} is replaced by the equivalent 
explicit loop - {{SignatureValue}} check plus per-Reference 
{{Reference.validate()}} - with {{cacheReference}} toggled off for SwA 
attachment References. This is behaviour-preserving: the property is read per 
Reference at transform time, {{Reference.validate()}} is public JSR-105 and 
runs at most once per Reference, short-circuit semantics are kept, and 
{{DOMXMLSignature.validate()}} does nothing else WSS4J relies on (Manifest 
validation is gated on {{{}org.jcp.xml.dsig.validateManifests{}}}, which WSS4J 
never sets - and Manifests are rejected by {{checkBSPCompliance}} anyway).

The second half of the patch is required, not optional: with caching off, 
{{getDereferencedData()}} returns null, so {{buildProtectedRefs()}} must 
recognise attachment References by their Transform algorithm instead - toggling 
the flag without it makes every signed attachment fail verification with 
{{{}FAILED_CHECK{}}}. The resulting {{WSDataRef}} is byte-for-byte what the old 
code produced (same synthesised {{<attachment>}} element, same 
{{{}setAttachment(true){}}}).

Non-attachment References keep {{{}cacheReference=true{}}}, so element 
recovery, the STR-dereference path (WSS-222) and the anti-wrapping checks that 
depend on it are untouched. The Santuario {{Reference}} objects never escape 
{{{}SignatureProcessor{}}}, and ws-security-stax does not use the property.

Test results with the patch applied: {{mvn -pl ws-security-dom test}} - 563 
tests, 0 failures, 0 errors, 5 skipped (unchanged from baseline), including 
{{AttachmentTest}} (17), {{XOPAttachmentTest}} (13) and both negative 
attachment-signature tests; {{mvn -pl integration test}} - 15 tests, 0 
failures. The 200 MB verify above passes under {{-Xmx256m}} with a 14 MB peak.
h3. Notes
 * This is WSS4J's defect, not Santuario's: the {{XMLValidateContext}} javadoc 
specifies {{cacheReference}} as one switch for both caches, so {{DOMReference}} 
cannot drop the digest-input half without breaking its contract. WSS4J asks for 
the full cache and then consumes only half of it. (Decoupling the caches 
upstream fixes the symptom too, but would have to be a new opt-in property plus 
a Santuario release; the WSS4J patch needs neither.)
 * Distinct from WSS-638, which is about {{processAttachment()}} buffering a 
non-mark-capable *source* stream via 
{{BufferedInputStream.mark(Integer.MAX_VALUE)}} and is still reproducible here 
on both the sign and verify paths. That one has a caller-side workaround (a 
disk-backed, mark-capable stream); this one has none, because the digest-side 
cache retains the attachment independently of the source stream. Both need 
fixing for large attachments to verify in bounded memory.
 * The {{cacheReference}} call has been present since the {{org.apache.wss4j}} 
rename (WSS4J 2.0, commit f647a91bd), so 2.x and 3.x are affected too; measured 
on 4.0.1 only.


> SignatureProcessor retains the entire attachment in heap when verifying an 
> SwA signed attachment (cacheReference)
> -----------------------------------------------------------------------------------------------------------------
>
>                 Key: WSS-727
>                 URL: https://issues.apache.org/jira/browse/WSS-727
>             Project: WSS4J
>          Issue Type: Bug
>          Components: WSS4J Core
>    Affects Versions: 4.0.1
>            Reporter: Ivan Novak
>            Assignee: Colm O hEigeartaigh
>            Priority: Major
>         Attachments: LargeAttachmentRepro-1.java, wss4j-patch-1.diff
>
>
> Verifying a SOAP message with a signed SwA attachment requires roughly *2.3x 
> the attachment size* in heap, regardless of whether the attachment is backed 
> by disk and regardless of whether the caller supplies a properly streaming, 
> mark-capable source stream. Signing the same message is streaming and needs a 
> constant ~20 MB.
> The cause is {{SignatureProcessor.verifyXMLSignature()}} (ws-security-dom, 
> line 353):
> {code:java}
> XMLValidateContext context = new DOMValidateContext(key, elem);
> context.setProperty("javax.xml.crypto.dsig.cacheReference", Boolean.TRUE);
> {code}
> In Santuario that single property controls two unrelated caches 
> ({{{}DOMReference.transform(){}}}, xmlsec 4.0.4, DOMReference.java:474-476 
> and :566):
>  * the dereferenced {{Data}} - cheap: for a {{cid:}} URI the resolver returns 
> a zero-length input, and it is the half WSS4J actually consumes 
> ({{{}buildProtectedRefs(){}}} uses it to work out what each Reference 
> covered, and for an attachment to synthesise the {{<attachment>}} marker 
> element and set {{{}WSDataRef.setAttachment(true){}}});
>  * the pre-digested input - {{DigesterOutputStream}} retains every octet fed 
> to the digest in an {{UnsyncByteArrayOutputStream}} that grows by doubling, 
> plus one more full array copy in {{{}getInputStream(){}}}.
> For an SwA reference the last transform 
> ({{{}AttachmentContentSignatureTransform{}}}) writes the attachment straight 
> into that stream, so a 200 MB attachment costs a 256 MB backing array plus a 
> 200 MB copy - ~456 MB live at once. The buffered copy is dead weight: its 
> only consumer is {{{}Reference.getDigestInputStream(){}}}, which nothing in 
> WSS4J calls.
> h3. Reproduction
> Attached: {{LargeAttachmentRepro.java}} (self-contained, no test-jar 
> dependencies).
> {code:java}
> java -Xmx<N> -cp <ws-security-dom test classpath> LargeAttachmentRepro 
> sign|verify 200 <workdir>
> {code}
> It signs / verifies a 200 MB {{application/octet-stream}} attachment read 
> from a file. The attachment's source stream implements 
> {{{}mark(){}}}/{{{}reset(){}}} by re-opening the file, so it contributes zero 
> heap - any growth observed is the digest-side cache alone.
> Environment: WSS4J 4.0.1, xmlsec 4.0.4, Temurin JDK 26.0.1, Linux.
> ||scenario||-Xmx||result||
> |sign, 200 MB attachment|256m|OK, peak used heap 22 MB|
> |verify, 200 MB attachment|256m|OutOfMemoryError|
> |verify, 200 MB attachment|400m|OutOfMemoryError (peak 394 MB)|
> |verify, 200 MB attachment|512m|OK, peak used heap *466 MB*|
> |verify, with attached patch|256m|OK, peak used heap *14 MB*|
> {code:java}
> java.lang.OutOfMemoryError: Java heap space
>       at 
> org.apache.xml.security.utils.UnsyncByteArrayOutputStream.expandSize(UnsyncByteArrayOutputStream.java:113)
>       at 
> org.apache.xml.security.utils.UnsyncByteArrayOutputStream.write(UnsyncByteArrayOutputStream.java:63)
>       at 
> org.apache.jcp.xml.dsig.internal.DigesterOutputStream.write(DigesterOutputStream.java:83)
>       at 
> org.apache.xml.security.utils.UnsyncBufferedOutputStream.write(UnsyncBufferedOutputStream.java:52)
>       at 
> org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.processAttachment(AttachmentContentSignatureTransform.java:218)
>       at 
> org.apache.wss4j.dom.transform.AttachmentContentSignatureTransform.transform(AttachmentContentSignatureTransform.java:122)
>       at 
> org.apache.jcp.xml.dsig.internal.dom.DOMTransform.transform(DOMTransform.java:170)
>       at 
> org.apache.jcp.xml.dsig.internal.dom.DOMReference.transform(DOMReference.java:488)
>       at 
> org.apache.jcp.xml.dsig.internal.dom.DOMReference.validate(DOMReference.java:415)
>       at 
> org.apache.jcp.xml.dsig.internal.dom.DOMXMLSignature.validate(DOMXMLSignature.java:295)
>       at 
> org.apache.wss4j.dom.processor.SignatureProcessor.verifyXMLSignature(SignatureProcessor.java:381)
>       ...
> {code}
> h3. Proposed fix (attached: {{wss4j-patch.diff}} - WSS4J-only, no Santuario 
> change required)
> The outer {{xmlSignature.validate(context)}} is replaced by the equivalent 
> explicit loop - {{SignatureValue}} check plus per-Reference 
> {{Reference.validate()}} - with {{cacheReference}} toggled off for SwA 
> attachment References. This is behaviour-preserving: the property is read per 
> Reference at transform time, {{Reference.validate()}} is public JSR-105 and 
> runs at most once per Reference, short-circuit semantics are kept, and 
> {{DOMXMLSignature.validate()}} does nothing else WSS4J relies on (Manifest 
> validation is gated on {{{}org.jcp.xml.dsig.validateManifests{}}}, which 
> WSS4J never sets - and Manifests are rejected by {{checkBSPCompliance}} 
> anyway).
> The second half of the patch is required, not optional: with caching off, 
> {{getDereferencedData()}} returns null, so {{buildProtectedRefs()}} must 
> recognise attachment References by their Transform algorithm instead - 
> toggling the flag without it makes every signed attachment fail verification 
> with {{{}FAILED_CHECK{}}}. The resulting {{WSDataRef}} is byte-for-byte what 
> the old code produced (same synthesised {{<attachment>}} element, same 
> {{{}setAttachment(true){}}}).
> Non-attachment References keep {{{}cacheReference=true{}}}, so element 
> recovery, the STR-dereference path (WSS-222) and the anti-wrapping checks 
> that depend on it are untouched. The Santuario {{Reference}} objects never 
> escape {{{}SignatureProcessor{}}}, and ws-security-stax does not use the 
> property.
> Test results with the patch applied: {{mvn -pl ws-security-dom test}} - 563 
> tests, 0 failures, 0 errors, 5 skipped (unchanged from baseline), including 
> {{AttachmentTest}} (17), {{XOPAttachmentTest}} (13) and both negative 
> attachment-signature tests; {{mvn -pl integration test}} - 15 tests, 0 
> failures. The 200 MB verify above passes under {{-Xmx256m}} with a 14 MB peak.
> h3. Notes
>  * This is WSS4J's defect, not Santuario's: the {{XMLValidateContext}} 
> javadoc specifies {{cacheReference}} as one switch for both caches, so 
> {{DOMReference}} cannot drop the digest-input half without breaking its 
> contract. WSS4J asks for the full cache and then consumes only half of it. 
> (Decoupling the caches upstream fixes the symptom too, but would have to be a 
> new opt-in property plus a Santuario release; the WSS4J patch needs neither.)
>  * Distinct from WSS-638, which is about {{processAttachment()}} buffering a 
> non-mark-capable *source* stream via 
> {{BufferedInputStream.mark(Integer.MAX_VALUE)}} and is still reproducible 
> here on both the sign and verify paths. That one has a caller-side workaround 
> (a disk-backed, mark-capable stream); this one has none, because the 
> digest-side cache retains the attachment independently of the source stream. 
> Both need fixing for large attachments to verify in bounded memory.
>  * The {{cacheReference}} call has been present since the 
> {{org.apache.wss4j}} rename (WSS4J 2.0, commit f647a91bd), so 2.x and 3.x are 
> affected too; measured on 4.0.1 only.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to