This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/policy-fixes in repository https://gitbox.apache.org/repos/asf/ws-wss4j.git
commit cb9a1ea748dac8090687c7dfdfe26f701566fbaa Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 10 09:36:19 2026 +0100 Reject duplicate C14N settings + Detect unknown top-level policy assertions --- .../apache/wss4j/policy/model/AlgorithmSuite.java | 7 + .../wss4j/policy/stax/enforcer/PolicyEnforcer.java | 65 +++++- .../policy/stax/test/UnknownAssertionsTest.java | 247 +++++++++++++++++++++ 3 files changed, 308 insertions(+), 11 deletions(-) diff --git a/policy/src/main/java/org/apache/wss4j/policy/model/AlgorithmSuite.java b/policy/src/main/java/org/apache/wss4j/policy/model/AlgorithmSuite.java index f574de05c..f8639e484 100644 --- a/policy/src/main/java/org/apache/wss4j/policy/model/AlgorithmSuite.java +++ b/policy/src/main/java/org/apache/wss4j/policy/model/AlgorithmSuite.java @@ -748,6 +748,7 @@ public class AlgorithmSuite extends AbstractSecurityAssertion implements PolicyC //this means that if we have a compact policy only the first alternative is visible //in contrary to a normalized policy where just one alternative exists if (alternatives.hasNext()) { + boolean c14nSeen = false; List<Assertion> assertions = alternatives.next(); for (Assertion assertion : assertions) { String assertionName = assertion.getName().getLocalPart(); @@ -772,6 +773,12 @@ public class AlgorithmSuite extends AbstractSecurityAssertion implements PolicyC } C14NType c14NType = C14NType.lookUp(assertionName); if (c14NType != null) { + // Reject duplicate C14N assertions instead of letting the last one win, + // consistent with the sibling SOAPNorm/STR/XPath branches below + if (c14nSeen) { + throw new IllegalArgumentException(SPConstants.ERR_INVALID_POLICY); + } + c14nSeen = true; algorithmSuite.setC14n(c14NType); continue; } diff --git a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java index 061ba2f88..97b6c8c5e 100644 --- a/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java +++ b/ws-security-policy-stax/src/main/java/org/apache/wss4j/policy/stax/enforcer/PolicyEnforcer.java @@ -45,6 +45,7 @@ import org.apache.wss4j.policy.model.AbstractSecurityAssertion; import org.apache.wss4j.policy.model.AbstractSymmetricAsymmetricBinding; import org.apache.wss4j.policy.model.AbstractToken; import org.apache.wss4j.policy.model.AlgorithmSuite; +import org.apache.wss4j.policy.model.BootstrapPolicy; import org.apache.wss4j.policy.model.ContentEncryptedElements; import org.apache.wss4j.policy.model.EncryptedElements; import org.apache.wss4j.policy.model.EncryptedParts; @@ -128,6 +129,16 @@ public class PolicyEnforcer implements SecurityEventListener { private static final transient org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(PolicyEnforcer.class); + /** + * System property controlling how policy assertions which this enforcer cannot enforce + * are handled. The default ("false") logs a warning and skips the assertion (the + * historical behaviour). When set to "true", building the assertion state map fails + * with a WSSPolicyException instead, so that the server cannot silently enforce less + * than the policy it advertises. + */ + public static final String FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY = + "org.apache.wss4j.policy.failOnUnsupportedAssertions"; + private static final QName SOAP11_FAULT = new QName(WSSConstants.NS_SOAP11, "Fault"); private static final QName SOAP12_FAULT = new QName(WSSConstants.NS_SOAP12, "Fault"); @@ -231,7 +242,7 @@ public class PolicyEnforcer implements SecurityEventListener { if (policyOperator instanceof ExactlyOne) { assertionStateMap.add(new HashMap<SecurityEventConstants.Event, Map<Assertion, List<Assertable>>>()); - buildAssertionStateMap(curPolicyComponent, assertionStateMap, alternative++); + buildAssertionStateMap(curPolicyComponent, assertionStateMap, alternative++, false); } else { buildAssertionStateMap(curPolicyComponent, assertionStateMap); } @@ -246,7 +257,8 @@ public class PolicyEnforcer implements SecurityEventListener { PolicyComponent policyComponent, List<Map<SecurityEventConstants.Event, Map<Assertion, List<Assertable>>>> assertionStateMap, - int alternative + int alternative, + boolean nestedPolicy ) throws WSSPolicyException { if (policyComponent instanceof PolicyOperator) { PolicyOperator policyOperator = (PolicyOperator) policyComponent; @@ -254,7 +266,7 @@ public class PolicyEnforcer implements SecurityEventListener { Iterator<PolicyComponent> policyComponentIterator = policyComponents.iterator(); while (policyComponentIterator.hasNext()) { PolicyComponent curPolicyComponent = policyComponentIterator.next(); - buildAssertionStateMap(curPolicyComponent, assertionStateMap, alternative); + buildAssertionStateMap(curPolicyComponent, assertionStateMap, alternative, nestedPolicy); } } else if (policyComponent instanceof AbstractSecurityAssertion) { AbstractSecurityAssertion abstractSecurityAssertion = (AbstractSecurityAssertion) policyComponent; @@ -278,14 +290,46 @@ public class PolicyEnforcer implements SecurityEventListener { } if (abstractSecurityAssertion instanceof PolicyContainingAssertion) { buildAssertionStateMap(((PolicyContainingAssertion) abstractSecurityAssertion).getPolicy(), - assertionStateMap, alternative); - } - } else if (!(policyComponent instanceof PrimitiveAssertion)) { + assertionStateMap, alternative, true); + } + } else if (policyComponent instanceof PrimitiveAssertion) { + if (!nestedPolicy) { + // A top-level PrimitiveAssertion is an assertion which no registered builder + // converted into a security policy model object - a namespace typo, an SP + // version mismatch, or a custom/unknown assertion. It cannot be enforced by + // this layer: the server would silently enforce less than the policy it + // advertises. Warn by default; fail if configured to do so. + handleUnknownAssertion(((PrimitiveAssertion) policyComponent).getName().toString()); + } + // Inside a PolicyContainingAssertion's nested policy the remaining + // PrimitiveAssertions are the standard leaves (e.g. sp:Basic256, sp:Strict, + // sp:IncludeTimestamp, sp:WssX509V3Token10) which the parent model's + // parseNestedPolicy already consumed by name and which are enforced through the + // parent's assertion state. They are not unknown, so they are deliberately + // skipped here (the historical behaviour). + } else { throw new WSSPolicyException("Unsupported PolicyComponent: " + policyComponent + " type: " + policyComponent.getType()); } } + /** + * Handle a policy assertion which this enforcer cannot enforce. By default a warning is + * logged and the assertion is skipped (the historical behaviour). If the + * {@link #FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY} system property is set to "true", a + * WSSPolicyException is thrown instead, so that a policy advertising more than the + * enforcer can verify is rejected at build time rather than silently under-enforced. + */ + private void handleUnknownAssertion(String assertionName) throws WSSPolicyException { + String msg = "Policy assertion " + assertionName + + " is unknown to the PolicyEnforcer and will NOT be enforced"; + if (Boolean.parseBoolean(System.getProperty(FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY, "false"))) { + throw new WSSPolicyException(msg); + } + LOG.warn("{}. Set the {} system property to \"true\" to reject unsupported assertions instead.", + msg, FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY); + } + private void addAssertionState(Map<Assertion, List<Assertable>> assertables, Assertion keyAssertion, Assertable assertable) { @@ -502,15 +546,14 @@ public class PolicyEnforcer implements SecurityEventListener { policyAsserter.assertPolicy(new QName(namespace, SPConstants.SCOPE_POLICY_15)); } } + } else if (abstractSecurityAssertion instanceof PolicyContainingAssertion + || abstractSecurityAssertion instanceof BootstrapPolicy) { + policyAsserter.assertPolicy(abstractSecurityAssertion); } else { + handleUnknownAssertion(abstractSecurityAssertion.getName().toString()); policyAsserter.assertPolicy(abstractSecurityAssertion); } - /*else if (abstractSecurityAssertion instanceof AsymmetricBinding) { - } else if (abstractSecurityAssertion instanceof SymmetricBinding) { - } else if (abstractSecurityAssertion instanceof TransportBinding) { - } */ - return assertableList; } diff --git a/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/UnknownAssertionsTest.java b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/UnknownAssertionsTest.java new file mode 100644 index 000000000..f1404ee4d --- /dev/null +++ b/ws-security-policy-stax/src/test/java/org/apache/wss4j/policy/stax/test/UnknownAssertionsTest.java @@ -0,0 +1,247 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.wss4j.policy.stax.test; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.namespace.QName; + +import org.apache.wss4j.common.WSSPolicyException; +import org.apache.wss4j.common.ext.WSSecurityException; +import org.apache.wss4j.policy.stax.enforcer.PolicyEnforcer; +import org.apache.wss4j.stax.ext.WSSConstants; +import org.apache.wss4j.stax.impl.securityToken.HttpsSecurityTokenImpl; +import org.apache.wss4j.stax.impl.securityToken.X509SecurityTokenImpl; +import org.apache.wss4j.stax.securityEvent.HttpsTokenSecurityEvent; +import org.apache.wss4j.stax.securityEvent.OperationSecurityEvent; +import org.apache.wss4j.stax.securityEvent.RequiredElementSecurityEvent; +import org.apache.wss4j.stax.securityEvent.SignedPartSecurityEvent; +import org.apache.wss4j.stax.securityEvent.TimestampSecurityEvent; +import org.apache.wss4j.stax.securityEvent.X509TokenSecurityEvent; +import org.apache.wss4j.stax.securityToken.WSSecurityTokenConstants; +import org.apache.xml.security.stax.ext.XMLSecurityConstants; +import org.apache.xml.security.stax.securityEvent.ContentEncryptedElementSecurityEvent; +import org.apache.xml.security.stax.securityEvent.EncryptedElementSecurityEvent; +import org.apache.xml.security.stax.securityToken.InboundSecurityToken; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Tests for the handling of policy assertions which the PolicyEnforcer cannot enforce: + * unknown assertions must be warned about (or rejected, when the + * {@link PolicyEnforcer#FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY} system property is set), + * while stock policies - whose nested leaves and container assertions are enforced via + * the parent model's assertion states - must never be flagged. + */ +public class UnknownAssertionsTest extends AbstractPolicyTestBase { + + private static final String UNKNOWN_ASSERTION_POLICY = + "<foo:UnknownAssertion xmlns:foo=\"http://www.example.org/custom-assertions\"/>"; + + /** + * A stock transport binding (nested AlgorithmSuite/Basic256 and IncludeTimestamp + * leaves) must build and verify cleanly even with failOnUnsupportedAssertions + * enabled - the nested leaves are consumed by the parent model's parser and are + * not unknown. + */ + @Test + public void testStockTransportBindingPolicyWithFailOnUnsupportedAssertionsEnabled() throws Exception { + String policyString = + "<sp:TransportBinding xmlns:sp=\"http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702\" xmlns:sp3=\"http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200802\">\n" + + "<wsp:Policy xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\">\n" + + " <sp:AlgorithmSuite>\n" + + " <wsp:Policy>\n" + + " <sp:Basic256/>\n" + + " </wsp:Policy>\n" + + " </sp:AlgorithmSuite>\n" + + "<sp:IncludeTimestamp/>\n" + + "</wsp:Policy>\n" + + "</sp:TransportBinding>"; + System.setProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY, "true"); + try { + PolicyEnforcer policyEnforcer = buildAndStartPolicyEngine(policyString); + TimestampSecurityEvent timestampSecurityEvent = new TimestampSecurityEvent(); + policyEnforcer.registerSecurityEvent(timestampSecurityEvent); + + RequiredElementSecurityEvent requiredElementSecurityEvent = new RequiredElementSecurityEvent(); + List<QName> headerPath = new ArrayList<>(); + headerPath.addAll(WSSConstants.SOAP_11_WSSE_SECURITY_HEADER_PATH); + headerPath.add(WSSConstants.TAG_WSU_TIMESTAMP); + requiredElementSecurityEvent.setElementPath(headerPath); + policyEnforcer.registerSecurityEvent(requiredElementSecurityEvent); + + HttpsTokenSecurityEvent httpsTokenSecurityEvent = new HttpsTokenSecurityEvent(); + HttpsSecurityTokenImpl httpsSecurityToken = new HttpsSecurityTokenImpl(true, "username"); + httpsSecurityToken.addTokenUsage(WSSecurityTokenConstants.TOKENUSAGE_MAIN_SIGNATURE); + httpsTokenSecurityEvent.setSecurityToken(httpsSecurityToken); + policyEnforcer.registerSecurityEvent(httpsTokenSecurityEvent); + + OperationSecurityEvent operationSecurityEvent = new OperationSecurityEvent(); + operationSecurityEvent.setOperation(WSDL_DEFINITIONS); + policyEnforcer.registerSecurityEvent(operationSecurityEvent); + + List<XMLSecurityConstants.ContentType> protectionOrder = new LinkedList<>(); + protectionOrder.add(XMLSecurityConstants.ContentType.SIGNATURE); + protectionOrder.add(XMLSecurityConstants.ContentType.ENCRYPTION); + EncryptedElementSecurityEvent encryptedElementSecurityEvent = + new EncryptedElementSecurityEvent(null, true, protectionOrder); + headerPath = new ArrayList<>(); + headerPath.addAll(WSSConstants.SOAP_11_WSSE_SECURITY_HEADER_PATH); + headerPath.add(WSSConstants.TAG_dsig_Signature); + encryptedElementSecurityEvent.setElementPath(headerPath); + policyEnforcer.registerSecurityEvent(encryptedElementSecurityEvent); + + encryptedElementSecurityEvent = new EncryptedElementSecurityEvent(null, true, protectionOrder); + headerPath = new ArrayList<>(); + headerPath.addAll(WSSConstants.SOAP_11_WSSE_SECURITY_HEADER_PATH); + headerPath.add(WSSConstants.TAG_WSSE11_SIG_CONF); + encryptedElementSecurityEvent.setElementPath(headerPath); + policyEnforcer.registerSecurityEvent(encryptedElementSecurityEvent); + + SignedPartSecurityEvent signedPartSecurityEvent = new SignedPartSecurityEvent(null, true, protectionOrder); + signedPartSecurityEvent.setElementPath(WSSConstants.SOAP_11_BODY_PATH); + policyEnforcer.registerSecurityEvent(signedPartSecurityEvent); + + policyEnforcer.doFinal(); + } finally { + System.clearProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY); + } + } + + /** + * A stock asymmetric binding whose tokens are carried in InitiatorToken/RecipientToken + * wrappers (with nested X509Token leaves such as sp:WssX509V3Token11) must build and + * verify cleanly even with failOnUnsupportedAssertions enabled - the token wrappers + * are recognized containers whose nested policies are enforced via the + * PolicyContainingAssertion recursion. + */ + @Test + public void testAsymmetricBindingWithTokenWrappersAndFailOnUnsupportedAssertionsEnabled() throws Exception { + String policyString = + "<sp:AsymmetricBinding xmlns:sp=\"http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702\" xmlns:sp3=\"http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200802\">\n" + + "<wsp:Policy xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\">\n" + + "<sp:InitiatorToken>\n" + + " <wsp:Policy>\n" + + " <sp:X509Token>\n" + + " <sp:IssuerName>CN=transmitter,OU=swssf,C=CH</sp:IssuerName>\n" + + " <wsp:Policy xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\">\n" + + " <sp:RequireThumbprintReference/>\n" + + " <sp:WssX509V3Token11/>\n" + + " </wsp:Policy>\n" + + " </sp:X509Token>\n" + + " </wsp:Policy>\n" + + "</sp:InitiatorToken>\n" + + "<sp:RecipientToken>\n" + + " <wsp:Policy>\n" + + " <sp:X509Token>\n" + + " <sp:IssuerName>CN=transmitter,OU=swssf,C=CH</sp:IssuerName>\n" + + " <wsp:Policy xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy\">\n" + + " <sp:RequireThumbprintReference/>\n" + + " <sp:WssX509V3Token11/>\n" + + " </wsp:Policy>\n" + + " </sp:X509Token>\n" + + " </wsp:Policy>\n" + + "</sp:RecipientToken>\n" + + " <sp:AlgorithmSuite>\n" + + " <wsp:Policy>\n" + + " <sp:Basic256/>\n" + + " </wsp:Policy>\n" + + " </sp:AlgorithmSuite>\n" + + "</wsp:Policy>\n" + + "</sp:AsymmetricBinding>"; + System.setProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY, "true"); + try { + PolicyEnforcer policyEnforcer = buildAndStartPolicyEngine(policyString); + X509TokenSecurityEvent initiatorX509TokenSecurityEvent = new X509TokenSecurityEvent(); + X509SecurityTokenImpl securityToken = getX509Token(WSSecurityTokenConstants.X509V3Token); + securityToken.addTokenUsage(WSSecurityTokenConstants.TOKENUSAGE_MAIN_SIGNATURE); + initiatorX509TokenSecurityEvent.setSecurityToken(securityToken); + policyEnforcer.registerSecurityEvent(initiatorX509TokenSecurityEvent); + + X509TokenSecurityEvent recipientX509TokenSecurityEvent = new X509TokenSecurityEvent(); + securityToken = getX509Token(WSSecurityTokenConstants.X509V3Token); + securityToken.addTokenUsage(WSSecurityTokenConstants.TOKENUSAGE_MAIN_ENCRYPTION); + recipientX509TokenSecurityEvent.setSecurityToken(securityToken); + policyEnforcer.registerSecurityEvent(recipientX509TokenSecurityEvent); + + List<XMLSecurityConstants.ContentType> protectionOrder = new LinkedList<>(); + protectionOrder.add(XMLSecurityConstants.ContentType.SIGNATURE); + protectionOrder.add(XMLSecurityConstants.ContentType.ENCRYPTION); + SignedPartSecurityEvent signedPartSecurityEvent = + new SignedPartSecurityEvent( + (InboundSecurityToken)recipientX509TokenSecurityEvent.getSecurityToken(), true, protectionOrder); + signedPartSecurityEvent.setElementPath(WSSConstants.SOAP_11_BODY_PATH); + policyEnforcer.registerSecurityEvent(signedPartSecurityEvent); + + ContentEncryptedElementSecurityEvent contentEncryptedElementSecurityEvent = + new ContentEncryptedElementSecurityEvent( + (InboundSecurityToken)recipientX509TokenSecurityEvent.getSecurityToken(), true, protectionOrder); + contentEncryptedElementSecurityEvent.setElementPath(WSSConstants.SOAP_11_BODY_PATH); + policyEnforcer.registerSecurityEvent(contentEncryptedElementSecurityEvent); + + OperationSecurityEvent operationSecurityEvent = new OperationSecurityEvent(); + operationSecurityEvent.setOperation(WSDL_DEFINITIONS); + policyEnforcer.registerSecurityEvent(operationSecurityEvent); + + policyEnforcer.doFinal(); + } finally { + System.clearProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY); + } + } + + /** + * A genuinely unknown top-level assertion (no registered builder) is skipped with a + * warning by default - the historical behaviour - so the policy still builds. + */ + @Test + public void testUnknownPrimitiveAssertionIsSkippedByDefault() throws Exception { + PolicyEnforcer policyEnforcer = buildAndStartPolicyEngine(UNKNOWN_ASSERTION_POLICY); + OperationSecurityEvent operationSecurityEvent = new OperationSecurityEvent(); + operationSecurityEvent.setOperation(WSDL_DEFINITIONS); + policyEnforcer.registerSecurityEvent(operationSecurityEvent); + policyEnforcer.doFinal(); + } + + /** + * The same unknown top-level assertion must fail the policy build when + * failOnUnsupportedAssertions is enabled. + */ + @Test + public void testUnknownPrimitiveAssertionFailsWhenConfigured() throws Exception { + System.setProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY, "true"); + try { + PolicyEnforcer policyEnforcer = buildAndStartPolicyEngine(UNKNOWN_ASSERTION_POLICY); + OperationSecurityEvent operationSecurityEvent = new OperationSecurityEvent(); + operationSecurityEvent.setOperation(WSDL_DEFINITIONS); + try { + policyEnforcer.registerSecurityEvent(operationSecurityEvent); + fail("Exception expected"); + } catch (WSSecurityException e) { + assertTrue(e.getCause() instanceof WSSPolicyException); + assertTrue(e.getCause().getMessage().contains("UnknownAssertion")); + } + } finally { + System.clearProperty(PolicyEnforcer.FAIL_ON_UNSUPPORTED_ASSERTIONS_PROPERTY); + } + } +}
