This is an automated email from the ASF dual-hosted git repository.

robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git

commit 1671a68660df39459bf22f7b00309e2a8e782117
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Aug 4 05:17:16 2026 -1000

    Harden five anonymous-reachable surfaces reported privately on 2026-07-30
    
    Private security report against 54de37f455. Not for push until the PMC has
    ruled on CVE assignment and the embargo lifts.
    
    WS-Addressing ReplyTo/FaultTo egress (report F1): screen inbound 
non-anonymous
    response endpoints in AddressingInHandler rather than in the transport 
sender,
    which also carries legitimate client-initiated calls. Default policy refuses
    loopback, link-local, and private destinations; 
allowNonAnonymousResponseEndpoints
    =false gives the stricter posture CXF made its default in PR #3279.
    
    Swagger UI XSS (F2): the Host reached an inline script unescaped. 
URI.create,
    added in 212440b614, does not neutralise it -- the breakout payload is a 
legal
    authority and round-trips unchanged. Validate the host, encode for the 
script
    context, and nonce the inline block under a CSP.
    
    OpenAPI servers[].url (F3): add openapi.serverBaseUrl so an operator can 
pin the
    published URL, and validate the host when falling back to the request.
    
    Request-body ceilings (F4): the multipart and form-urlencoded builders read 
the
    transport stream directly, so the container's post limit never applies. 
Bound
    both, configurable per service; -1 restores the old behaviour.
    
    exposeServiceMetadata (F5): the .xsd/.wsdl file routes, HTTPWorker's 
named-WSDL
    route, and the whole OpenAPI/MCP generator ignored the gate their siblings 
apply.
    
    Co-Authored-By: Claude Fable 5 <[email protected]>
---
 .../handlers/addressing/AddressingInHandler.java   |  23 +++
 .../addressing/ResponseEndpointPolicy.java         | 230 +++++++++++++++++++++
 .../addressing/ResponseEndpointPolicyTest.java     | 147 +++++++++++++
 modules/kernel/conf/axis2.xml                      |  34 +++
 .../apache/axis2/builder/BoundedInputStream.java   |  90 ++++++++
 .../axis2/builder/MultipartFormDataBuilder.java    |  25 ++-
 .../apache/axis2/builder/RequestSizeLimits.java    |  99 +++++++++
 .../axis2/builder/XFormURLEncodedBuilder.java      |   9 +-
 .../axis2/builder/RequestSizeLimitsTest.java       | 130 ++++++++++++
 .../apache/axis2/openapi/OpenApiConfiguration.java |  13 ++
 .../apache/axis2/openapi/OpenApiSpecGenerator.java |  61 +++++-
 .../org/apache/axis2/openapi/RequestUrlPolicy.java | 112 ++++++++++
 .../org/apache/axis2/openapi/SwaggerUIHandler.java | 118 ++++++++++-
 .../apache/axis2/openapi/RequestUrlPolicyTest.java | 103 +++++++++
 .../apache/axis2/openapi/SwaggerUIHandlerTest.java |  57 +++++
 .../apache/axis2/transport/http/HTTPWorker.java    |  10 +
 .../apache/axis2/transport/http/ListingAgent.java  |   8 +
 17 files changed, 1245 insertions(+), 24 deletions(-)

diff --git 
a/modules/addressing/src/org/apache/axis2/handlers/addressing/AddressingInHandler.java
 
b/modules/addressing/src/org/apache/axis2/handlers/addressing/AddressingInHandler.java
index 708da90c91..1c6ffc1547 100644
--- 
a/modules/addressing/src/org/apache/axis2/handlers/addressing/AddressingInHandler.java
+++ 
b/modules/addressing/src/org/apache/axis2/handlers/addressing/AddressingInHandler.java
@@ -414,6 +414,7 @@ public class AddressingInHandler extends 
AbstractTemplatedHandler implements Add
         if (LoggingControl.debugLoggingAllowed && log.isTraceEnabled()) {
             log.trace("extractFaultToEPRInformation: Extracted FaultTo EPR: " 
+ epr);
         }
+        rejectDisallowedResponseEndpoint(epr, messageContext, soapHeaderBlock);
         soapHeaderBlock.setProcessed();
     }
 
@@ -430,9 +431,31 @@ public class AddressingInHandler extends 
AbstractTemplatedHandler implements Add
         if (LoggingControl.debugLoggingAllowed && log.isTraceEnabled()) {
             log.trace("extractReplyToEPRInformation: Extracted ReplyTo EPR: " 
+ epr);
         }
+        rejectDisallowedResponseEndpoint(epr, messageContext, soapHeaderBlock);
         soapHeaderBlock.setProcessed();
     }
 
+    /**
+     * Fault out when an inbound response endpoint names a destination the 
server
+     * is not willing to send to.
+     *
+     * <p>Only inbound server-side messages are screened. On the client side 
these
+     * same headers arrive on a response the client itself solicited, and the
+     * addresses in them are not attacker-chosen in the same way.
+     */
+    private void rejectDisallowedResponseEndpoint(EndpointReference epr,
+                                                  MessageContext 
messageContext,
+                                                  SOAPHeaderBlock 
soapHeaderBlock)
+            throws AxisFault {
+        if (!messageContext.isServerSide()) {
+            return;
+        }
+        if (!ResponseEndpointPolicy.isAllowed(epr, messageContext)) {
+            AddressingFaultsHelper
+                    .triggerInvalidEPRFault(messageContext, 
soapHeaderBlock.getLocalName());
+        }
+    }
+
     private void extractFromEPRInformation(SOAPHeaderBlock soapHeaderBlock,
                                            String addressingNamespace,
                                            MessageContext messageContext) 
throws AxisFault {
diff --git 
a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java
 
b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java
new file mode 100644
index 0000000000..81b330e802
--- /dev/null
+++ 
b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java
@@ -0,0 +1,230 @@
+/*
+ * 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.axis2.handlers.addressing;
+
+import org.apache.axis2.addressing.EndpointReference;
+import org.apache.axis2.context.MessageContext;
+import org.apache.axis2.description.Parameter;
+import org.apache.axis2.util.JavaUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.net.InetAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Decides whether a {@code wsa:ReplyTo} or {@code wsa:FaultTo} endpoint
+ * reference taken off an inbound message may be used as the destination of a
+ * server-initiated send.
+ *
+ * <p>A non-anonymous response endpoint is a spec-defined WS-Addressing feature
+ * — the SOAP binding says a response SHOULD be delivered to it — but the same
+ * specification's security considerations put the burden of deciding whether 
to
+ * honour one on the receiver, noting that "great care should be taken before
+ * honoring a [reply endpoint] or [fault endpoint] to avoid inadvertent
+ * participation in the activities of malicious SOAP message senders". Where
+ * WS-Security is not engaged to bind the EPR to a trusted issuer, an anonymous
+ * caller otherwise chooses the address the server connects to.
+ *
+ * <p>Three parameters control this, resolved through the usual Axis2 chain
+ * (service, service group, then {@code axis2.xml}):
+ *
+ * <dl>
+ * <dt>{@code allowNonAnonymousResponseEndpoints} (default {@code true})</dt>
+ * <dd>Set to {@code false} to refuse every non-anonymous response endpoint, so
+ * replies and faults only ever travel back down the inbound connection. This 
is
+ * the strictest posture and the right one for a deployment that does not use
+ * decoupled or dual-channel responses.</dd>
+ *
+ * <dt>{@code blockPrivateNetworkResponseEndpoints} (default {@code true})</dt>
+ * <dd>Rejects response endpoints that resolve to loopback, link-local (which
+ * covers the cloud instance-metadata addresses), site-local/RFC-1918, or
+ * wildcard addresses — the destinations that turn this into a probe of the
+ * server's own network rather than a genuine reply.</dd>
+ *
+ * <dt>{@code allowedResponseEndpointHosts} (no default)</dt>
+ * <dd>A comma-separated host allow-list. When set, a response endpoint host 
must
+ * appear in it, which supersedes the network-range check.</dd>
+ * </dl>
+ */
+final class ResponseEndpointPolicy {
+
+    private static final Log log = 
LogFactory.getLog(ResponseEndpointPolicy.class);
+
+    static final String ALLOW_NON_ANONYMOUS = 
"allowNonAnonymousResponseEndpoints";
+    static final String BLOCK_PRIVATE_NETWORKS = 
"blockPrivateNetworkResponseEndpoints";
+    static final String ALLOWED_HOSTS = "allowedResponseEndpointHosts";
+
+    /** Only transports that carry a genuine reply are worth honouring. */
+    private static final Set<String> ALLOWED_SCHEMES =
+            new HashSet<String>(Arrays.asList("http", "https"));
+
+    private ResponseEndpointPolicy() {
+    }
+
+    /**
+     * Whether the server may send to this response endpoint.
+     *
+     * <p>Anonymous and null endpoints are always permitted: they mean "reply 
on
+     * the inbound connection" and drive no outbound connection at all.
+     *
+     * @param epr the ReplyTo or FaultTo taken from the inbound message
+     * @param messageContext the inbound message, for parameter resolution
+     * @return true if the endpoint may be used as a send destination
+     */
+    static boolean isAllowed(EndpointReference epr, MessageContext 
messageContext) {
+        if (epr == null || epr.hasAnonymousAddress() || epr.hasNoneAddress()) {
+            return true;
+        }
+        String address = epr.getAddress();
+        if (address == null || address.trim().isEmpty()) {
+            return true;
+        }
+
+        if (!booleanParameter(messageContext, ALLOW_NON_ANONYMOUS, true)) {
+            log.warn("Rejecting non-anonymous WS-Addressing response endpoint: 
"
+                    + ALLOW_NON_ANONYMOUS + " is false");
+            return false;
+        }
+
+        URI uri;
+        try {
+            uri = new URI(address.trim());
+        } catch (URISyntaxException e) {
+            log.warn("Rejecting unparseable WS-Addressing response endpoint 
address");
+            return false;
+        }
+
+        String scheme = uri.getScheme();
+        if (scheme == null || 
!ALLOWED_SCHEMES.contains(scheme.toLowerCase(Locale.ENGLISH))) {
+            log.warn("Rejecting WS-Addressing response endpoint with 
unsupported scheme: " + scheme);
+            return false;
+        }
+
+        String host = uri.getHost();
+        if (host == null || host.isEmpty()) {
+            log.warn("Rejecting WS-Addressing response endpoint with no host 
component");
+            return false;
+        }
+
+        String allowedHosts = stringParameter(messageContext, ALLOWED_HOSTS);
+        if (allowedHosts != null && !allowedHosts.trim().isEmpty()) {
+            for (String allowed : allowedHosts.split(",")) {
+                if (allowed.trim().equalsIgnoreCase(host)) {
+                    return true;
+                }
+            }
+            log.warn("Rejecting WS-Addressing response endpoint host absent 
from "
+                    + ALLOWED_HOSTS);
+            return false;
+        }
+
+        if (booleanParameter(messageContext, BLOCK_PRIVATE_NETWORKS, true)) {
+            return !resolvesToRestrictedAddress(host);
+        }
+
+        return true;
+    }
+
+    /**
+     * Whether a host resolves to an address the server should not be induced 
to
+     * connect to. Every address the name resolves to has to pass, so a name 
with
+     * one public and one loopback record is rejected.
+     *
+     * <p>This resolves the name to inspect it and the connection resolves it
+     * again later, so a hostile DNS server could in principle answer 
differently
+     * the second time. Closing that fully means resolving once and connecting 
to
+     * the pinned address, which the transport layer does not currently 
support;
+     * the check still removes the direct-IP and static-name cases that make 
this
+     * reachable in practice.
+     */
+    private static boolean resolvesToRestrictedAddress(String host) {
+        InetAddress[] addresses;
+        try {
+            addresses = InetAddress.getAllByName(host);
+        } catch (UnknownHostException e) {
+            // Unresolvable here means the send would fail anyway, so refuse
+            // rather than pass an unknown destination through.
+            log.warn("Rejecting WS-Addressing response endpoint with 
unresolvable host");
+            return true;
+        }
+        for (int i = 0; i < addresses.length; i++) {
+            InetAddress address = addresses[i];
+            if (address.isLoopbackAddress()
+                    || address.isLinkLocalAddress()
+                    || address.isSiteLocalAddress()
+                    || address.isAnyLocalAddress()
+                    || address.isMulticastAddress()
+                    || isUniqueLocalIPv6(address)
+                    || isSharedAddressSpace(address)) {
+                log.warn("Rejecting WS-Addressing response endpoint resolving 
to a "
+                        + "loopback, link-local, or private address");
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** IPv6 unique local addresses, fc00::/7, which isSiteLocalAddress 
misses. */
+    private static boolean isUniqueLocalIPv6(InetAddress address) {
+        byte[] bytes = address.getAddress();
+        return bytes.length == 16 && (bytes[0] & 0xfe) == 0xfc;
+    }
+
+    /** RFC 6598 carrier-grade NAT space, 100.64.0.0/10. */
+    private static boolean isSharedAddressSpace(InetAddress address) {
+        byte[] bytes = address.getAddress();
+        return bytes.length == 4
+                && (bytes[0] & 0xff) == 100
+                && (bytes[1] & 0xc0) == 0x40;
+    }
+
+    private static boolean booleanParameter(MessageContext messageContext, 
String name,
+                                            boolean defaultValue) {
+        if (messageContext == null) {
+            return defaultValue;
+        }
+        Parameter parameter = messageContext.getParameter(name);
+        if (parameter == null || parameter.getValue() == null) {
+            return defaultValue;
+        }
+        if (defaultValue) {
+            return !JavaUtils.isFalseExplicitly(parameter.getValue());
+        }
+        return JavaUtils.isTrueExplicitly(parameter.getValue());
+    }
+
+    private static String stringParameter(MessageContext messageContext, 
String name) {
+        if (messageContext == null) {
+            return null;
+        }
+        Parameter parameter = messageContext.getParameter(name);
+        if (parameter == null || parameter.getValue() == null) {
+            return null;
+        }
+        return parameter.getValue().toString();
+    }
+}
diff --git 
a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java
 
b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java
new file mode 100644
index 0000000000..fd1d26f06a
--- /dev/null
+++ 
b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.axis2.handlers.addressing;
+
+import junit.framework.TestCase;
+import org.apache.axis2.addressing.AddressingConstants;
+import org.apache.axis2.addressing.EndpointReference;
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.MessageContext;
+import org.apache.axis2.description.Parameter;
+import org.apache.axis2.engine.AxisConfiguration;
+
+/**
+ * Unit tests for the egress policy applied to inbound WS-Addressing ReplyTo 
and
+ * FaultTo endpoint references.
+ */
+public class ResponseEndpointPolicyTest extends TestCase {
+
+    private AxisConfiguration axisConfiguration;
+    private MessageContext messageContext;
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        axisConfiguration = new AxisConfiguration();
+        ConfigurationContext configurationContext = new 
ConfigurationContext(axisConfiguration);
+        messageContext = configurationContext.createMessageContext();
+        messageContext.setServerSide(true);
+    }
+
+    private void setParameter(String name, String value) throws Exception {
+        axisConfiguration.addParameter(new Parameter(name, value));
+    }
+
+    /**
+     * The anonymous address means "reply on this connection" and drives no
+     * outbound send, so it must never be blocked.
+     */
+    public void testAnonymousAddressIsAlwaysAllowed() {
+        EndpointReference anonymous =
+                new 
EndpointReference(AddressingConstants.Final.WSA_ANONYMOUS_URL);
+        assertTrue(ResponseEndpointPolicy.isAllowed(anonymous, 
messageContext));
+        assertTrue(ResponseEndpointPolicy.isAllowed(null, messageContext));
+    }
+
+    /**
+     * The cloud instance-metadata address is the canonical target for this 
class
+     * of SSRF and is link-local, so the default policy must refuse it.
+     */
+    public void testInstanceMetadataAddressIsBlockedByDefault() {
+        EndpointReference metadata =
+                new 
EndpointReference("http://169.254.169.254/latest/meta-data/";);
+        assertFalse(ResponseEndpointPolicy.isAllowed(metadata, 
messageContext));
+    }
+
+    public void testLoopbackAndPrivateAddressesAreBlockedByDefault() {
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://127.0.0.1:8080/sink";), 
messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://10.1.2.3/internal";), 
messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://192.168.1.10/admin";), 
messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://172.16.5.5/admin";), 
messageContext));
+    }
+
+    /**
+     * Only transports that can carry a genuine reply are honoured, so the
+     * scheme-based SSRF pivots are refused before any host check.
+     */
+    public void testNonHttpSchemesAreRejected() {
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("file:///etc/passwd"), messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("gopher://example.com/1";), 
messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("jar:http://example.com/a.jar!/";), 
messageContext));
+    }
+
+    /**
+     * A routable public address is still permitted by default, so decoupled
+     * responses to a genuine external endpoint keep working.
+     */
+    public void testPublicAddressIsAllowedByDefault() {
+        EndpointReference publicEpr = new 
EndpointReference("http://192.0.2.25/replies";);
+        assertTrue(ResponseEndpointPolicy.isAllowed(publicEpr, 
messageContext));
+    }
+
+    /**
+     * The strict posture — the equivalent of what CXF made its default — 
refuses
+     * every non-anonymous response endpoint.
+     */
+    public void testNonAnonymousCanBeDisabledEntirely() throws Exception {
+        setParameter(ResponseEndpointPolicy.ALLOW_NON_ANONYMOUS, "false");
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://192.0.2.25/replies";), 
messageContext));
+        // The anonymous case must still work, or in-out messaging breaks.
+        assertTrue(ResponseEndpointPolicy.isAllowed(
+                new 
EndpointReference(AddressingConstants.Final.WSA_ANONYMOUS_URL),
+                messageContext));
+    }
+
+    /**
+     * An explicit allow-list supersedes the network-range check, so a 
deployment
+     * that really does reply into its own network can permit exactly that 
host.
+     */
+    public void testAllowListPermitsAnOtherwiseBlockedHost() throws Exception {
+        setParameter(ResponseEndpointPolicy.ALLOWED_HOSTS, 
"replies.example.com, 127.0.0.1");
+        assertTrue(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://127.0.0.1:8080/sink";), 
messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://192.0.2.25/replies";), 
messageContext));
+    }
+
+    /**
+     * Turning the range check off restores the pre-2.0.2 behaviour for 
operators
+     * who need it.
+     */
+    public void testPrivateRangeCheckCanBeDisabled() throws Exception {
+        setParameter(ResponseEndpointPolicy.BLOCK_PRIVATE_NETWORKS, "false");
+        assertTrue(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://127.0.0.1:8080/sink";), 
messageContext));
+    }
+
+    public void testMalformedAddressIsRejected() {
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http://[not a uri"), messageContext));
+        assertFalse(ResponseEndpointPolicy.isAllowed(
+                new EndpointReference("http:///no-host";), messageContext));
+    }
+}
diff --git a/modules/kernel/conf/axis2.xml b/modules/kernel/conf/axis2.xml
index 0f82059704..63c6a06f62 100644
--- a/modules/kernel/conf/axis2.xml
+++ b/modules/kernel/conf/axis2.xml
@@ -45,6 +45,40 @@
     -->
     <parameter name="exposeServiceMetadata">true</parameter>
 
+    <!--
+    Ceilings on request bodies read directly off the transport stream by the
+    multipart/form-data and x-www-form-urlencoded message builders. Those
+    builders bypass the servlet container's own post-size limit, so without
+    these an anonymous caller chooses the allocation. Sizes are in bytes; -1
+    restores the unbounded behaviour of releases before 2.0.2. These may also 
be
+    set per service in services.xml.
+    -->
+    <parameter name="multipartMaxRequestSize">104857600</parameter>
+    <parameter name="multipartMaxFileSize">104857600</parameter>
+    <parameter name="formUrlEncodedMaxRequestSize">2097152</parameter>
+
+    <!--
+    Policy for WS-Addressing wsa:ReplyTo / wsa:FaultTo endpoints on inbound
+    server-side messages. A non-anonymous response endpoint makes the server
+    open a connection to an address the caller chose, so unless WS-Security is
+    engaged to bind that endpoint reference to a trusted issuer, these bound
+    where the server can be induced to connect.
+
+    blockPrivateNetworkResponseEndpoints (default true) rejects response
+    endpoints resolving to loopback, link-local (including cloud
+    instance-metadata addresses), or private ranges.
+
+    Set allowNonAnonymousResponseEndpoints to false to refuse decoupled
+    responses entirely - the right setting unless this deployment actually uses
+    dual-channel or decoupled WS-Addressing responses.
+
+    allowedResponseEndpointHosts, when set to a comma-separated host list,
+    restricts response endpoints to exactly those hosts.
+    -->
+    <parameter name="allowNonAnonymousResponseEndpoints">true</parameter>
+    <parameter name="blockPrivateNetworkResponseEndpoints">true</parameter>
+    <!--<parameter 
name="allowedResponseEndpointHosts">replies.example.com</parameter>-->
+
 
     <!--Uncomment if you want to plugin your own attachments lifecycle 
implementation -->
     <!--<attachmentsLifecycleManager 
class="org.apache.axiom.attachments.lifecycle.impl.LifecycleManagerImpl"/>-->
diff --git 
a/modules/kernel/src/org/apache/axis2/builder/BoundedInputStream.java 
b/modules/kernel/src/org/apache/axis2/builder/BoundedInputStream.java
new file mode 100644
index 0000000000..46f67fbcf5
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/builder/BoundedInputStream.java
@@ -0,0 +1,90 @@
+/*
+ * 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.axis2.builder;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An {@link InputStream} that fails once more than a fixed number of bytes has
+ * been read from it.
+ *
+ * <p>This deliberately throws rather than reporting end-of-stream at the 
limit,
+ * the way {@code org.apache.commons.io.input.BoundedInputStream} does. A 
builder
+ * that silently saw EOF would parse a truncated body and hand the service a
+ * partial message, which is worse than rejecting an over-sized request.
+ */
+public class BoundedInputStream extends FilterInputStream {
+
+    private final long maxBytes;
+    private long bytesRead;
+
+    /**
+     * Wrap a stream, unless no bound was requested.
+     *
+     * @param in the stream to bound
+     * @param maxBytes the ceiling in bytes, or {@link 
RequestSizeLimits#UNLIMITED}
+     * @return a bounded view of the stream, or {@code in} itself when 
unbounded
+     */
+    public static InputStream wrap(InputStream in, long maxBytes) {
+        if (in == null || maxBytes < 0) {
+            return in;
+        }
+        return new BoundedInputStream(in, maxBytes);
+    }
+
+    public BoundedInputStream(InputStream in, long maxBytes) {
+        super(in);
+        this.maxBytes = maxBytes;
+    }
+
+    public int read() throws IOException {
+        int b = in.read();
+        if (b != -1) {
+            count(1);
+        }
+        return b;
+    }
+
+    public int read(byte[] b, int off, int len) throws IOException {
+        int n = in.read(b, off, len);
+        if (n > 0) {
+            count(n);
+        }
+        return n;
+    }
+
+    public long skip(long n) throws IOException {
+        long skipped = in.skip(n);
+        if (skipped > 0) {
+            count(skipped);
+        }
+        return skipped;
+    }
+
+    private void count(long n) throws IOException {
+        bytesRead += n;
+        if (bytesRead > maxBytes) {
+            throw new IOException("Request body exceeds the configured maximum 
of "
+                    + maxBytes + " bytes");
+        }
+    }
+}
diff --git 
a/modules/kernel/src/org/apache/axis2/builder/MultipartFormDataBuilder.java 
b/modules/kernel/src/org/apache/axis2/builder/MultipartFormDataBuilder.java
index 58dbb31fe2..8a909ce683 100644
--- a/modules/kernel/src/org/apache/axis2/builder/MultipartFormDataBuilder.java
+++ b/modules/kernel/src/org/apache/axis2/builder/MultipartFormDataBuilder.java
@@ -67,17 +67,13 @@ public class MultipartFormDataBuilder implements Builder {
             throw new AxisFault("Cannot create DocumentElement without 
HttpServletRequest");
         }
 
-        // TODO: Do check ContentLength for the max size,
-        //       but it can't be configured anywhere.
-        //       I think that it cant be configured at web.xml or axis2.xml.
-
         String charSetEncoding = 
(String)messageContext.getProperty(Constants.Configuration.CHARACTER_SET_ENCODING);
         if (charSetEncoding == null) {
             charSetEncoding = request.getCharacterEncoding();
         }
 
         try {
-            parameterMap = getParameterMap(request, charSetEncoding);
+            parameterMap = getParameterMap(request, charSetEncoding, 
messageContext);
             return BuilderUtil.buildsoapMessage(messageContext, parameterMap,
                                                 
OMAbstractFactory.getSOAP12Factory());
 
@@ -88,12 +84,13 @@ public class MultipartFormDataBuilder implements Builder {
     }
 
     private MultipleEntryHashMap getParameterMap(HttpServletRequest request,
-                                                 String charSetEncoding)
+                                                 String charSetEncoding,
+                                                 MessageContext messageContext)
             throws FileUploadException {
 
         MultipleEntryHashMap parameterMap = new MultipleEntryHashMap();
 
-        List items = parseRequest(new JakartaServletRequestContext(request));
+        List items = parseRequest(new JakartaServletRequestContext(request), 
messageContext);
         Iterator iter = items.iterator();
         while (iter.hasNext()) {
             DiskFileItem diskFileItem = (DiskFileItem)iter.next();
@@ -116,16 +113,26 @@ public class MultipartFormDataBuilder implements Builder {
         return parameterMap;
     }
 
-    private static List parseRequest(JakartaServletRequestContext 
requestContext)
+    private static List parseRequest(JakartaServletRequestContext 
requestContext,
+                                     MessageContext messageContext)
             throws FileUploadException {
         // Create a factory for disk-based file items
        DiskFileItemFactory fileItemFactory = DiskFileItemFactory.builder()
                 .setCharset(StandardCharsets.UTF_8)
                 .get();
         JakartaServletFileUpload upload = new 
JakartaServletFileUpload<>(fileItemFactory);
-        // There must be a limit. 
+        // There must be a limit.
         // This is for contentType="multipart/form-data"
         upload.setMaxFileCount(1L);
+        // Bound the body as well as the part count. commons-fileupload2 reads 
the
+        // raw transport stream, so the container's own post limit never 
applies
+        // and an unbounded parse lets the client choose the allocation.
+        upload.setMaxSize(RequestSizeLimits.resolve(messageContext,
+                RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE,
+                RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE));
+        upload.setMaxFileSize(RequestSizeLimits.resolve(messageContext,
+                RequestSizeLimits.MULTIPART_MAX_FILE_SIZE,
+                RequestSizeLimits.DEFAULT_MULTIPART_MAX_FILE_SIZE));
         // Parse the request
         return upload.parseRequest(requestContext);
     }
diff --git a/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java 
b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
new file mode 100644
index 0000000000..28b48682a3
--- /dev/null
+++ b/modules/kernel/src/org/apache/axis2/builder/RequestSizeLimits.java
@@ -0,0 +1,99 @@
+/*
+ * 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.axis2.builder;
+
+import org.apache.axis2.context.MessageContext;
+import org.apache.axis2.description.Parameter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Size ceilings applied by the message builders that read a request body
+ * directly from the transport stream.
+ *
+ * <p>Those builders consume the raw {@code InputStream} rather than the 
servlet
+ * parameter API, so a container-level post limit (Tomcat's {@code 
maxPostSize},
+ * for example) never sees the body and does not constrain it. Without a 
ceiling
+ * here an anonymous client can size the allocation itself.
+ *
+ * <p>Each limit resolves through the usual Axis2 parameter chain — message,
+ * operation, service, service group, then {@code axis2.xml} — so a deployment
+ * that legitimately accepts large uploads can raise it for one service without
+ * loosening the global default. A value of {@code -1} restores the unbounded
+ * behaviour.
+ */
+public final class RequestSizeLimits {
+
+    private static final Log log = LogFactory.getLog(RequestSizeLimits.class);
+
+    /** Ceiling on a whole multipart/form-data request. */
+    public static final String MULTIPART_MAX_REQUEST_SIZE = 
"multipartMaxRequestSize";
+
+    /** Ceiling on any single part within a multipart/form-data request. */
+    public static final String MULTIPART_MAX_FILE_SIZE = 
"multipartMaxFileSize";
+
+    /** Ceiling on an application/x-www-form-urlencoded request body. */
+    public static final String FORM_URLENCODED_MAX_REQUEST_SIZE = 
"formUrlEncodedMaxRequestSize";
+
+    /** 100 MB: generous for document and attachment uploads, but finite. */
+    public static final long DEFAULT_MULTIPART_MAX_REQUEST_SIZE = 100L * 1024 
* 1024;
+
+    /** 100 MB, matching the whole-request ceiling for the single-part case. */
+    public static final long DEFAULT_MULTIPART_MAX_FILE_SIZE = 100L * 1024 * 
1024;
+
+    /** 2 MB: form encoding is for field data, not bulk transfer. */
+    public static final long DEFAULT_FORM_URLENCODED_MAX_REQUEST_SIZE = 2L * 
1024 * 1024;
+
+    /** Sentinel for "no ceiling", matching the commons-fileupload2 
convention. */
+    public static final long UNLIMITED = -1L;
+
+    private RequestSizeLimits() {
+    }
+
+    /**
+     * Resolve a size limit for the message being built.
+     *
+     * @param messageContext the message being built; null yields the default
+     * @param parameterName one of the parameter name constants on this class
+     * @param defaultValue the limit to apply when the parameter is not set
+     * @return the configured limit in bytes, or {@link #UNLIMITED}
+     */
+    public static long resolve(MessageContext messageContext, String 
parameterName, long defaultValue) {
+        if (messageContext == null) {
+            return defaultValue;
+        }
+        Parameter parameter = messageContext.getParameter(parameterName);
+        if (parameter == null || parameter.getValue() == null) {
+            return defaultValue;
+        }
+        String value = parameter.getValue().toString().trim();
+        if (value.isEmpty()) {
+            return defaultValue;
+        }
+        try {
+            long limit = Long.parseLong(value);
+            return limit < 0 ? UNLIMITED : limit;
+        } catch (NumberFormatException e) {
+            log.warn("Ignoring non-numeric value '" + value + "' for parameter 
'"
+                    + parameterName + "'; using the default of " + 
defaultValue + " bytes");
+            return defaultValue;
+        }
+    }
+}
diff --git 
a/modules/kernel/src/org/apache/axis2/builder/XFormURLEncodedBuilder.java 
b/modules/kernel/src/org/apache/axis2/builder/XFormURLEncodedBuilder.java
index 7b4667331b..310b32fcb8 100644
--- a/modules/kernel/src/org/apache/axis2/builder/XFormURLEncodedBuilder.java
+++ b/modules/kernel/src/org/apache/axis2/builder/XFormURLEncodedBuilder.java
@@ -109,10 +109,17 @@ public class XFormURLEncodedBuilder implements Builder {
             query = requestURL.substring(index + 1);
         }
 
+        // The body is read straight off the transport stream, so the 
container's
+        // own post limit never sees it. Bound it here or the client picks the 
size
+        // of the map we build below.
+        long maxRequestSize = RequestSizeLimits.resolve(messageContext,
+                RequestSizeLimits.FORM_URLENCODED_MAX_REQUEST_SIZE,
+                RequestSizeLimits.DEFAULT_FORM_URLENCODED_MAX_REQUEST_SIZE);
+
         extractParametersFromRequest(parameterMap, query, 
queryParameterSeparator,
                                      (String) messageContext.getProperty(
                                              
Constants.Configuration.CHARACTER_SET_ENCODING),
-                                     inputStream);
+                                     BoundedInputStream.wrap(inputStream, 
maxRequestSize));
 
         messageContext.setProperty(Constants.REQUEST_PARAMETER_MAP, 
parameterMap);
 
diff --git 
a/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java 
b/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java
new file mode 100644
index 0000000000..20be388192
--- /dev/null
+++ b/modules/kernel/test/org/apache/axis2/builder/RequestSizeLimitsTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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.axis2.builder;
+
+import junit.framework.TestCase;
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.MessageContext;
+import org.apache.axis2.description.Parameter;
+import org.apache.axis2.engine.AxisConfiguration;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit tests for the request-body ceilings applied by the message builders 
that
+ * read the transport stream directly.
+ */
+public class RequestSizeLimitsTest extends TestCase {
+
+    private AxisConfiguration axisConfiguration;
+    private MessageContext messageContext;
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        axisConfiguration = new AxisConfiguration();
+        ConfigurationContext configurationContext = new 
ConfigurationContext(axisConfiguration);
+        messageContext = configurationContext.createMessageContext();
+    }
+
+    public void testDefaultsApplyWhenUnconfigured() {
+        assertEquals(RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE,
+                RequestSizeLimits.resolve(messageContext,
+                        RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE,
+                        RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE));
+    }
+
+    public void testConfiguredValueOverridesDefault() throws Exception {
+        axisConfiguration.addParameter(
+                new Parameter(RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE, 
"4096"));
+        assertEquals(4096L, RequestSizeLimits.resolve(messageContext,
+                RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE,
+                RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE));
+    }
+
+    /** A negative value is the documented way back to the old unbounded 
behaviour. */
+    public void testNegativeValueMeansUnlimited() throws Exception {
+        axisConfiguration.addParameter(
+                new Parameter(RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE, 
"-1"));
+        assertEquals(RequestSizeLimits.UNLIMITED, 
RequestSizeLimits.resolve(messageContext,
+                RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE,
+                RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE));
+    }
+
+    /** A typo in axis2.xml must not silently remove the ceiling. */
+    public void testGarbageValueFallsBackToTheDefault() throws Exception {
+        axisConfiguration.addParameter(
+                new Parameter(RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE, 
"not-a-number"));
+        assertEquals(RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE,
+                RequestSizeLimits.resolve(messageContext,
+                        RequestSizeLimits.MULTIPART_MAX_REQUEST_SIZE,
+                        RequestSizeLimits.DEFAULT_MULTIPART_MAX_REQUEST_SIZE));
+    }
+
+    public void testBoundedStreamPassesBodiesWithinTheLimit() throws Exception 
{
+        InputStream in = BoundedInputStream.wrap(
+                new ByteArrayInputStream(new byte[512]), 1024);
+        assertEquals(512, readFully(in));
+    }
+
+    public void testBoundedStreamRejectsAnOversizedBody() {
+        InputStream in = BoundedInputStream.wrap(
+                new ByteArrayInputStream(new byte[4096]), 1024);
+        try {
+            readFully(in);
+            fail("Reading past the ceiling should have failed");
+        } catch (IOException expected) {
+            assertTrue("The failure should name the limit",
+                    expected.getMessage().contains("1024"));
+        }
+    }
+
+    /**
+     * Failing rather than reporting end-of-stream matters: a truncated body 
would
+     * otherwise be parsed as if it were the whole message.
+     */
+    public void testBoundedStreamFailsRatherThanTruncatingByteAtATime() {
+        InputStream in = BoundedInputStream.wrap(new ByteArrayInputStream(new 
byte[8]), 4);
+        try {
+            for (int i = 0; i < 8; i++) {
+                in.read();
+            }
+            fail("Reading past the ceiling should have failed");
+        } catch (IOException expected) {
+            // expected
+        }
+    }
+
+    public void testUnlimitedLeavesTheStreamUnwrapped() {
+        InputStream original = new ByteArrayInputStream(new byte[8]);
+        assertSame(original, BoundedInputStream.wrap(original, 
RequestSizeLimits.UNLIMITED));
+    }
+
+    private int readFully(InputStream in) throws IOException {
+        byte[] buffer = new byte[256];
+        int total = 0;
+        int n;
+        while ((n = in.read(buffer, 0, buffer.length)) != -1) {
+            total += n;
+        }
+        return total;
+    }
+}
diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
index fac1d542bf..58227d32f5 100644
--- 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiConfiguration.java
@@ -127,6 +127,15 @@ public class OpenApiConfiguration {
     /** Security scheme definitions */
     private Map<String, SecurityScheme> securityDefinitions = new HashMap<>();
 
+    /**
+     * Explicit base URL to publish as the specification's {@code 
servers[].url}.
+     * When unset the URL is derived from the request, which means it follows 
the
+     * client-supplied Host; deployments behind a proxy that forwards an
+     * untrusted Host should set this so the served specification always points
+     * clients at the real origin.
+     */
+    private String serverBaseUrl;
+
     // ========== Swagger UI Configuration ==========
 
     /** Whether to support Swagger UI */
@@ -239,6 +248,7 @@ public class OpenApiConfiguration {
         description = getProperty(props, "openapi.description", description);
         version = getProperty(props, "openapi.version", version);
         termsOfServiceUrl = getProperty(props, "openapi.termsOfServiceUrl", 
termsOfServiceUrl);
+        serverBaseUrl = getProperty(props, "openapi.serverBaseUrl", 
serverBaseUrl);
 
         // Contact
         contactName = getProperty(props, "openapi.contact.name", contactName);
@@ -361,6 +371,9 @@ public class OpenApiConfiguration {
     public String getTermsOfServiceUrl() { return termsOfServiceUrl; }
     public void setTermsOfServiceUrl(String termsOfServiceUrl) { 
this.termsOfServiceUrl = termsOfServiceUrl; }
 
+    public String getServerBaseUrl() { return serverBaseUrl; }
+    public void setServerBaseUrl(String serverBaseUrl) { this.serverBaseUrl = 
serverBaseUrl; }
+
     public String getContactName() { return contactName; }
     public void setContactName(String contactName) { this.contactName = 
contactName; }
 
diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
index b9d67ff7b8..03b5d2a785 100644
--- 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/OpenApiSpecGenerator.java
@@ -229,12 +229,42 @@ public class OpenApiSpecGenerator {
     private List<Server> createServerList(HttpServletRequest request) {
         List<Server> servers = new ArrayList<>();
 
+        // An explicitly configured base URL always wins: clients driven by 
this
+        // specification (Swagger UI "Try it out", MCP tools) send subsequent
+        // requests to servers[].url, so an operator behind a proxy needs to be
+        // able to pin it rather than have it follow the inbound Host.
+        String configuredBaseUrl = configuration.getServerBaseUrl();
+        if (configuredBaseUrl != null && !configuredBaseUrl.isEmpty()) {
+            Server server = new Server();
+            server.setUrl(configuredBaseUrl);
+            server.setDescription("Configured server");
+            servers.add(server);
+            return servers;
+        }
+
         if (request != null) {
             // Build server URL from request
             String scheme = request.getScheme();
             String serverName = request.getServerName();
             int serverPort = request.getServerPort();
             String contextPath = request.getContextPath();
+            if (contextPath == null) {
+                contextPath = "";
+            }
+
+            if (!RequestUrlPolicy.isSafeHost(serverName)) {
+                // The Host is client-supplied. Publishing a malformed one 
would
+                // point every client that reads this specification at it, so 
fall
+                // back to a relative server URL, which OpenAPI 3 resolves 
against
+                // the location the document was retrieved from.
+                log.warn("Rejecting malformed Host header for the OpenAPI 
server URL; "
+                        + "publishing a relative server URL instead");
+                Server server = new Server();
+                server.setUrl(contextPath.isEmpty() ? "/" : contextPath);
+                server.setDescription("Current server");
+                servers.add(server);
+                return servers;
+            }
 
             StringBuilder serverUrl = new StringBuilder();
             serverUrl.append(scheme).append("://").append(serverName);
@@ -245,9 +275,7 @@ public class OpenApiSpecGenerator {
                 serverUrl.append(":").append(serverPort);
             }
 
-            if (contextPath != null && !contextPath.isEmpty()) {
-                serverUrl.append(contextPath);
-            }
+            serverUrl.append(contextPath);
 
             Server server = new Server();
             server.setUrl(serverUrl.toString());
@@ -463,6 +491,23 @@ public class OpenApiSpecGenerator {
                serviceName.contains("AdminService");
     }
 
+    /**
+     * Whether a service's metadata may be published to an anonymous caller.
+     *
+     * <p>Reads the same {@code exposeServiceMetadata} parameter the HTTP
+     * transport's {@code ?wsdl}, {@code ?wsdl2} and {@code ?xsd} routes 
consult,
+     * resolved through the usual service, service-group, {@code axis2.xml} 
chain.
+     */
+    private boolean canExposeServiceMetadata(AxisService service) {
+        // Fully qualified: io.swagger.v3.oas.models.parameters.Parameter is 
the
+        // Parameter this class otherwise deals in.
+        org.apache.axis2.description.Parameter exposeServiceMetadata =
+                service.getParameter("exposeServiceMetadata");
+        return exposeServiceMetadata == null
+                || !org.apache.axis2.util.JavaUtils.isFalseExplicitly(
+                        exposeServiceMetadata.getValue());
+    }
+
     /**
      * Check if a service should be included based on configuration filters.
      * Exclusion is evaluated before inclusion: a service listed in
@@ -477,6 +522,15 @@ public class OpenApiSpecGenerator {
             return false;
         }
 
+        // An administrator who set exposeServiceMetadata=false has already 
said
+        // this service's shape is not for anonymous callers. The generated 
spec
+        // publishes strictly more than ?wsdl does — operation names, paths, 
and
+        // reflected request/response schemas — so it has to honour the same 
gate.
+        if (!canExposeServiceMetadata(service)) {
+            log.debug("Skipping service with exposeServiceMetadata=false: " + 
serviceName);
+            return false;
+        }
+
         String servicePackage = getServicePackage(service);
 
         // If readAllResources is false, check specific resource 
classes/packages
@@ -1451,6 +1505,7 @@ public class OpenApiSpecGenerator {
             for (AxisService service : services.values()) {
                 String svcName = service.getName();
                 if (isSystemService(service)) continue;
+                if (!shouldIncludeService(service)) continue;
 
                 // URI: logical identifier for the resource in the MCP 
protocol.
                 // Uses the "axis2://" scheme so clients can distinguish these
diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/RequestUrlPolicy.java 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/RequestUrlPolicy.java
new file mode 100644
index 0000000000..725c901e2b
--- /dev/null
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/RequestUrlPolicy.java
@@ -0,0 +1,112 @@
+/*
+ * 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.axis2.openapi;
+
+import java.util.regex.Pattern;
+
+/**
+ * Validation and output-encoding helpers for values taken from the inbound
+ * request that end up in documents this module serves — the Swagger UI page
+ * and the OpenAPI/MCP specifications.
+ *
+ * <p>{@code HttpServletRequest.getServerName()} returns the client-supplied
+ * Host (or the proxy-supplied forwarded host where the container is configured
+ * to honour it), so it is attacker-controlled. Two separate protections apply:
+ * {@link #isSafeHost} keeps a host that is not a syntactically valid 
registered
+ * name or IP literal out of served URLs entirely, and
+ * {@link #escapeJavaScriptString} encodes whatever does get emitted for the
+ * JavaScript string context it lands in.
+ */
+final class RequestUrlPolicy {
+
+    /**
+     * A registered name as permitted by RFC 1123: dot-separated labels of
+     * alphanumerics and hyphens, where a label neither starts nor ends with a
+     * hyphen. A single trailing dot (the fully-qualified form) is allowed.
+     */
+    private static final Pattern REGISTERED_NAME = Pattern.compile(
+            "[A-Za-z0-9](?:[A-Za-z0-9\\-]*[A-Za-z0-9])?"
+                    + 
"(?:\\.[A-Za-z0-9](?:[A-Za-z0-9\\-]*[A-Za-z0-9])?)*\\.?");
+
+    /**
+     * An IPv6 literal, with or without the surrounding brackets — containers
+     * differ on whether {@code getServerName()} strips them. The character set
+     * covers the embedded-IPv4 and zone-id forms as well.
+     */
+    private static final Pattern IP_LITERAL = Pattern.compile(
+            "\\[?[0-9A-Fa-f:.]{2,45}(?:%25?[A-Za-z0-9._~\\-]{1,32})?]?");
+
+    /** RFC 1035 caps a domain name at 255 octets including length prefixes. */
+    private static final int MAX_HOST_LENGTH = 253;
+
+    private RequestUrlPolicy() {
+    }
+
+    /**
+     * Whether a request-derived host may be placed into a served URL.
+     *
+     * @param host the value from {@code HttpServletRequest.getServerName()}
+     * @return true if the host is a syntactically valid registered name or IP
+     *         literal, false for null, empty, over-long, or malformed input
+     */
+    static boolean isSafeHost(String host) {
+        if (host == null || host.isEmpty() || host.length() > MAX_HOST_LENGTH) 
{
+            return false;
+        }
+        return REGISTERED_NAME.matcher(host).matches() || 
IP_LITERAL.matcher(host).matches();
+    }
+
+    /**
+     * Encode a value for interpolation into a quoted JavaScript string literal
+     * inside an inline {@code <script>} element.
+     *
+     * <p>Anything outside a conservative alphanumeric-plus-URL-punctuation set
+     * is emitted as a numeric escape, which covers the quote and backslash
+     * breakouts, the {@code </script>} breakout, and the U+2028/U+2029 line
+     * terminators that JavaScript treats as newlines inside a string.
+     *
+     * @param value the value to encode; null yields an empty string
+     * @return a value safe to place between quotes in a script element
+     */
+    static String escapeJavaScriptString(String value) {
+        if (value == null) {
+            return "";
+        }
+        StringBuilder out = new StringBuilder(value.length() + 16);
+        for (int i = 0; i < value.length(); i++) {
+            char c = value.charAt(i);
+            boolean unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 
'z')
+                    || (c >= '0' && c <= '9')
+                    || c == '-' || c == '_' || c == '.' || c == '~'
+                    || c == ':' || c == '/' || c == '?' || c == '#'
+                    || c == '@' || c == '!' || c == '$' || c == '*'
+                    || c == '+' || c == ',' || c == ';' || c == '='
+                    || c == '[' || c == ']' || c == '%';
+            if (unreserved) {
+                out.append(c);
+            } else if (c < 0x100) {
+                out.append(String.format("\\x%02x", (int) c));
+            } else {
+                out.append(String.format("\\u%04x", (int) c));
+            }
+        }
+        return out.toString();
+    }
+}
diff --git 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
index 4fb409c482..aaed480ae8 100644
--- 
a/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
+++ 
b/modules/openapi/src/main/java/org/apache/axis2/openapi/SwaggerUIHandler.java
@@ -30,7 +30,10 @@ import java.io.PrintWriter;
 import java.io.InputStream;
 import java.io.ByteArrayOutputStream;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Base64;
 import java.util.Map;
 
 /**
@@ -64,6 +67,12 @@ public class SwaggerUIHandler {
     private static final String SWAGGER_UI_ROOT = "/swagger-ui/";
     private static final String API_DOCS_PATH = "/api-docs/";
 
+    /** Origin the Swagger UI distribution is loaded from; needed by the CSP. 
*/
+    private static final String SWAGGER_UI_CDN_ORIGIN = "https://unpkg.com";;
+
+    /** Source of the per-response nonce for the inline initialisation script. 
*/
+    private static final SecureRandom NONCE_SOURCE = new SecureRandom();
+
     /**
      * Constructor with default configuration.
      */
@@ -109,11 +118,14 @@ public class SwaggerUIHandler {
         // Add security headers
         addSecurityHeaders(response);
 
+        String scriptNonce = newScriptNonce();
+        addContentSecurityPolicy(response, scriptNonce);
+
         // Build OpenAPI specification URL from configuration
         URI openApiUrl = buildOpenApiUrl(request);
 
         // Generate customized Swagger UI HTML
-        String swaggerHtml = generateSwaggerUIHtml(openApiUrl, request);
+        String swaggerHtml = generateSwaggerUIHtml(openApiUrl, request, 
scriptNonce);
 
         PrintWriter writer = response.getWriter();
         writer.write(swaggerHtml);
@@ -251,19 +263,31 @@ public class SwaggerUIHandler {
      * Build the base URL from request information.
      */
     private String buildBaseUrl(HttpServletRequest request) {
+        String contextPath = request.getContextPath();
+        if (contextPath == null) {
+            contextPath = "";
+        }
+
+        String serverName = request.getServerName();
+        if (!RequestUrlPolicy.isSafeHost(serverName)) {
+            // The Host is client-supplied. Rather than echo a malformed one 
back
+            // into the served page, degrade to an origin-relative URL, which 
the
+            // browser resolves against the page it already loaded.
+            log.warn("Rejecting malformed Host header for Swagger UI base URL; 
"
+                    + "serving an origin-relative specification URL instead");
+            return contextPath;
+        }
+
         StringBuilder url = new StringBuilder();
         url.append(request.getScheme()).append("://");
-        url.append(request.getServerName());
+        url.append(serverName);
 
         if ((request.getScheme().equals("http") && request.getServerPort() != 
80) ||
             (request.getScheme().equals("https") && request.getServerPort() != 
443)) {
             url.append(":").append(request.getServerPort());
         }
 
-        String contextPath = request.getContextPath();
-        if (contextPath != null && !contextPath.isEmpty()) {
-            url.append(contextPath);
-        }
+        url.append(contextPath);
 
         return url.toString();
     }
@@ -271,7 +295,7 @@ public class SwaggerUIHandler {
     /**
      * Generate HTML for Swagger UI page with configuration-driven 
customization.
      */
-    private String generateSwaggerUIHtml(URI openApiUrl, HttpServletRequest 
request) {
+    private String generateSwaggerUIHtml(URI openApiUrl, HttpServletRequest 
request, String scriptNonce) {
         String swaggerUiVersion = configuration.getSwaggerUiVersion() != null ?
                 configuration.getSwaggerUiVersion() : 
DEFAULT_SWAGGER_UI_VERSION;
 
@@ -311,7 +335,7 @@ public class SwaggerUIHandler {
             
.append(swaggerUiVersion).append("/swagger-ui-standalone-preset.js\"></script>\n");
 
         // Add Swagger UI initialization script
-        html.append(generateSwaggerUIScript(openApiUrl));
+        html.append(generateSwaggerUIScript(openApiUrl, scriptNonce));
 
         // Add custom JavaScript if configured
         if (swaggerUiConfig.getCustomJs() != null) {
@@ -382,15 +406,17 @@ public class SwaggerUIHandler {
     /**
      * Generate Swagger UI initialization script with configuration.
      */
-    private String generateSwaggerUIScript(URI openApiUrl) {
+    private String generateSwaggerUIScript(URI openApiUrl, String scriptNonce) 
{
         String configJs = swaggerUiConfig.toJavaScriptConfig();
 
         StringBuilder script = new StringBuilder();
-        script.append("    <script>\n")
+        script.append("    <script 
nonce=\"").append(scriptNonce).append("\">\n")
               .append("        window.onload = function() {\n")
               .append("            const ui = SwaggerUIBundle(Object.assign(")
               .append(configJs).append(", {\n")
-              .append("                url: 
'").append(openApiUrl.toString()).append("',\n")
+              .append("                url: '")
+              
.append(RequestUrlPolicy.escapeJavaScriptString(openApiUrl.toString()))
+              .append("',\n")
               .append("                dom_id: '#swagger-ui',\n")
               .append("                presets: [\n")
               .append("                    SwaggerUIBundle.presets.apis,\n")
@@ -454,6 +480,76 @@ public class SwaggerUIHandler {
         response.setHeader("X-XSS-Protection", "1; mode=block");
     }
 
+    /**
+     * Generate a single-use nonce for the page's inline initialisation script.
+     */
+    private String newScriptNonce() {
+        byte[] nonce = new byte[16];
+        NONCE_SOURCE.nextBytes(nonce);
+        return Base64.getEncoder().encodeToString(nonce);
+    }
+
+    /**
+     * Apply a Content-Security-Policy to the Swagger UI page.
+     *
+     * <p>Only the nonced initialisation script and the pinned Swagger UI
+     * distribution may execute, so script injected anywhere into this page 
does
+     * not run even if an encoding defect is reintroduced. Operator-configured
+     * custom CSS and JavaScript origins are added to the policy so that
+     * customised deployments keep working.
+     */
+    private void addContentSecurityPolicy(HttpServletResponse response, String 
scriptNonce) {
+        StringBuilder scriptSrc = new 
StringBuilder("'nonce-").append(scriptNonce)
+                .append("' ").append(SWAGGER_UI_CDN_ORIGIN);
+        StringBuilder styleSrc = new StringBuilder("'self' 'unsafe-inline' ")
+                .append(SWAGGER_UI_CDN_ORIGIN);
+
+        String customJsOrigin = originOf(swaggerUiConfig.getCustomJs());
+        if (customJsOrigin != null) {
+            scriptSrc.append(' ').append(customJsOrigin);
+        }
+        String customCssOrigin = originOf(swaggerUiConfig.getCustomCss());
+        if (customCssOrigin != null) {
+            styleSrc.append(' ').append(customCssOrigin);
+        }
+
+        response.setHeader("Content-Security-Policy",
+                "default-src 'self'; "
+                        + "script-src " + scriptSrc + "; "
+                        + "style-src " + styleSrc + "; "
+                        + "img-src 'self' data:; "
+                        + "connect-src 'self'; "
+                        + "base-uri 'none'; "
+                        + "frame-ancestors 'self'; "
+                        + "object-src 'none'");
+    }
+
+    /**
+     * Extract the {@code scheme://host[:port]} origin of an absolute URL, or
+     * null when the value is absent or not an absolute URL (a same-origin 
path,
+     * which {@code 'self'} already covers).
+     */
+    private String originOf(String url) {
+        if (url == null || url.isEmpty()) {
+            return null;
+        }
+        try {
+            URI uri = new URI(url);
+            if (uri.getScheme() == null || uri.getHost() == null) {
+                return null;
+            }
+            StringBuilder origin = new 
StringBuilder(uri.getScheme()).append("://").append(uri.getHost());
+            if (uri.getPort() != -1) {
+                origin.append(':').append(uri.getPort());
+            }
+            return origin.toString();
+        } catch (URISyntaxException e) {
+            log.warn("Ignoring unparseable custom resource URL when building 
the "
+                    + "Swagger UI Content-Security-Policy: " + e.getMessage());
+            return null;
+        }
+    }
+
     /**
      * Add CORS headers to response based on configuration.
      */
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/RequestUrlPolicyTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/RequestUrlPolicyTest.java
new file mode 100644
index 0000000000..ba4d873251
--- /dev/null
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/RequestUrlPolicyTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.axis2.openapi;
+
+import junit.framework.TestCase;
+
+import java.net.URI;
+
+/**
+ * Unit tests for the host validation and script-context encoding applied to
+ * request-derived values in served documents.
+ */
+public class RequestUrlPolicyTest extends TestCase {
+
+    public void testAcceptsOrdinaryHosts() {
+        assertTrue(RequestUrlPolicy.isSafeHost("localhost"));
+        assertTrue(RequestUrlPolicy.isSafeHost("api.example.com"));
+        
assertTrue(RequestUrlPolicy.isSafeHost("api-gateway.internal.example.com."));
+        assertTrue(RequestUrlPolicy.isSafeHost("192.0.2.10"));
+        assertTrue(RequestUrlPolicy.isSafeHost("[2001:db8::1]"));
+        assertTrue(RequestUrlPolicy.isSafeHost("2001:db8::1"));
+    }
+
+    public void testRejectsScriptBreakoutHosts() {
+        assertFalse(RequestUrlPolicy.isSafeHost("'-alert(document.domain)-'"));
+        assertFalse(RequestUrlPolicy.isSafeHost("x'+alert(1)+'"));
+        assertFalse(RequestUrlPolicy.isSafeHost("example.com/../evil"));
+        assertFalse(RequestUrlPolicy.isSafeHost("example.com evil.com"));
+        assertFalse(RequestUrlPolicy.isSafeHost("<script>"));
+    }
+
+    public void testRejectsEmptyAndOverlongHosts() {
+        assertFalse(RequestUrlPolicy.isSafeHost(null));
+        assertFalse(RequestUrlPolicy.isSafeHost(""));
+        StringBuilder overlong = new StringBuilder();
+        while (overlong.length() < 300) {
+            overlong.append("a");
+        }
+        assertFalse(RequestUrlPolicy.isSafeHost(overlong.toString()));
+    }
+
+    /**
+     * URI parsing is not a sanitiser here: the breakout payload is a legal
+     * authority and round-trips unchanged, which is why the encoder exists.
+     */
+    public void testUriCreateDoesNotNeutraliseTheBreakout() {
+        String hostile = "http://'-alert(document.domain)-'/openapi.json";
+        assertEquals(hostile, URI.create(hostile).toString());
+    }
+
+    public void testEscapesQuotesAndParentheses() {
+        String escaped = RequestUrlPolicy.escapeJavaScriptString(
+                "http://'-alert(document.domain)-'/openapi.json");
+        assertFalse("Quotes must not survive", escaped.contains("'"));
+        assertFalse("Call syntax must not survive", escaped.contains("("));
+        assertFalse("Call syntax must not survive", escaped.contains(")"));
+    }
+
+    public void testEscapesScriptElementBreakout() {
+        String escaped = 
RequestUrlPolicy.escapeJavaScriptString("</script><script>alert(1)</script>");
+        assertFalse("Angle brackets must not survive", escaped.contains("<"));
+        assertFalse("Angle brackets must not survive", escaped.contains(">"));
+    }
+
+    public void testEscapesLineTerminators() {
+        // U+2028 and U+2029 end a line inside a JavaScript string literal even
+        // though nothing else treats them as newlines, so they break out of 
the
+        // quoted URL exactly like a raw newline would.
+        String escaped = RequestUrlPolicy.escapeJavaScriptString(
+                "a\nb\r\u2028c\u2029d\\e");
+        assertFalse("Newline must not survive", escaped.contains("\n"));
+        assertFalse("Carriage return must not survive", 
escaped.contains("\r"));
+        assertFalse("U+2028 must not survive", escaped.contains("\u2028"));
+        assertFalse("U+2029 must not survive", escaped.contains("\u2029"));
+        assertFalse("A live backslash must not survive", 
escaped.contains("\\e"));
+    }
+
+    public void testLeavesOrdinaryUrlsReadable() {
+        String url = "https://api.example.com:8443/axis2/openapi.json";;
+        assertEquals(url, RequestUrlPolicy.escapeJavaScriptString(url));
+    }
+
+    public void testNullEncodesToEmptyString() {
+        assertEquals("", RequestUrlPolicy.escapeJavaScriptString(null));
+    }
+}
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIHandlerTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIHandlerTest.java
index 43a6cb283e..d6ad663264 100644
--- 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIHandlerTest.java
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIHandlerTest.java
@@ -257,6 +257,63 @@ public class SwaggerUIHandlerTest extends TestCase {
                 html.contains("supportedSubmitMethods") || 
html.contains("swagger-ui"));
     }
 
+    /**
+     * A Host header carrying a JavaScript breakout must not reach the inline
+     * script as executable syntax.
+     *
+     * <p>The payload closes the single-quoted string the specification URL 
sits
+     * in, concatenates a call, and reopens the string. It survives
+     * {@code URI.create} unchanged — every character in it is legal in an
+     * authority — so rejecting the host and encoding the output are what stop
+     * it, not URI parsing.
+     */
+    public void testSwaggerUIRejectsScriptBreakoutInHostHeader() throws 
Exception {
+        mockRequest.setServerName("'-alert(document.domain)-'");
+        mockRequest.setContextPath("/axis2");
+
+        handler.handleSwaggerUIRequest(mockRequest, mockResponse);
+        String html = mockResponse.getWriterContent();
+
+        assertFalse("Host must not reach the page as executable script",
+                html.contains("alert(document.domain)"));
+        assertFalse("The single-quote breakout must not survive",
+                html.contains("url: 'http://'-"));
+        assertTrue("The page should still render with a relative spec URL",
+                html.contains("url: '/axis2/openapi.json'"));
+    }
+
+    /**
+     * A well-formed Host is still used to build an absolute URL, so the
+     * rejection above is not simply breaking the normal path.
+     */
+    public void testSwaggerUIKeepsWellFormedHost() throws Exception {
+        mockRequest.setServerName("api.example.com");
+        mockRequest.setContextPath("/axis2");
+
+        handler.handleSwaggerUIRequest(mockRequest, mockResponse);
+        String html = mockResponse.getWriterContent();
+
+        assertTrue("A valid host should still produce an absolute URL",
+                html.contains("url: 
'http://api.example.com:8080/axis2/openapi.json'"));
+    }
+
+    /**
+     * The served page should carry a Content-Security-Policy whose script-src 
is
+     * restricted to the nonced inline block and the pinned distribution.
+     */
+    public void testSwaggerUISetsContentSecurityPolicy() throws Exception {
+        handler.handleSwaggerUIRequest(mockRequest, mockResponse);
+
+        String csp = mockResponse.getHeader("Content-Security-Policy");
+        assertNotNull("A CSP should be set on the Swagger UI page", csp);
+        assertTrue("The CSP should nonce the inline script", 
csp.contains("script-src 'nonce-"));
+        assertTrue("The CSP should not permit arbitrary inline script",
+                !csp.contains("script-src 'unsafe-inline'"));
+
+        String html = mockResponse.getWriterContent();
+        assertTrue("The inline script should carry the nonce", 
html.contains("<script nonce=\""));
+    }
+
     /**
      * Mock HttpServletRequest for testing.
      */
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
index c882ebf0dc..8eacd44c46 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/HTTPWorker.java
@@ -99,6 +99,12 @@ public class HTTPWorker implements Worker {
                             Iterator i = services.values().iterator();
                             while (i.hasNext()) {
                                 AxisService service = (AxisService) i.next();
+                                // Same exposure gate the ?wsdl/?wsdl2/?xsd 
routes
+                                // below apply: this reaches the same packaged
+                                // metadata, just addressed by file name.
+                                if (!canExposeServiceMetadata(service)) {
+                                    continue;
+                                }
                                 InputStream stream = 
HTTPTransportUtils.getMetaInfResourceAsStream(service, file);
                                 if (stream != null) {
                                     OutputStream out = 
response.getOutputStream();
@@ -377,6 +383,10 @@ public class HTTPWorker implements Worker {
         AxisService service = (AxisService) services.get(serviceName);
 
         if (service != null) {
+            if (!canExposeServiceMetadata(service)) {
+                response.setStatus(HttpStatus.SC_FORBIDDEN);
+                return true;
+            }
             response.setStatus(HttpStatus.SC_OK);
             response.setContentType("text/xml");
             service.printUserWSDL(response.getOutputStream(), wsdlName, ip);
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
index 2ca25eefd0..da283c2d8b 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/ListingAgent.java
@@ -111,6 +111,14 @@ public class ListingAgent extends AbstractAgent {
             Iterator<AxisService> i = services.values().iterator();
             while (i.hasNext()) {
                 AxisService service = (AxisService) i.next();
+                // This route reaches a service's packaged META-INF resource by
+                // file name, so it has to honour the same exposure gate as the
+                // ?wsdl/?wsdl2/?xsd routes below. Skipping rather than 
returning
+                // 403 keeps a hidden service from being distinguishable from 
an
+                // absent one, and lets a later service still serve the name.
+                if (!canExposeServiceMetadata(service)) {
+                    continue;
+                }
                 InputStream stream = 
HTTPTransportUtils.getMetaInfResourceAsStream(service, schema);
                 if (stream != null) {
                     OutputStream out = res.getOutputStream();

Reply via email to