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 00274eedfaf5e20b0160f4267271182646884170
Author: Robert Lazarski <[email protected]>
AuthorDate: Fri Sep 4 15:39:41 2026 -1000

    Pin the Swagger UI scripts, and stop naming exception classes in faults
    
    Two low-severity leaks on the JSON surface. The Swagger UI page loads its
    scripts from a CDN at a pinned version, which says which release to request 
but
    not that what arrived is that release, on a page built to drive 
authenticated
    API calls; the tags now carry SHA-384 integrity hashes, taken from two
    independent CDNs and confirmed identical. They are version-specific, so a
    deployment pinning another version supplies its own and is warned when it 
has
    not. Separately, an unhandled Throwable produced a fault reason of
    fully.qualified.Class: message regardless of 
sendStacktraceDetailsWithFaults,
    on JSON and REST responses as much as SOAP faults, since that catch-all 
serves
    every request the servlet handles; it is generic now, and still logged in 
full.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../apache/axis2/openapi/OpenApiConfiguration.java |  24 +++++
 .../org/apache/axis2/openapi/SwaggerUIHandler.java |  70 +++++++++++++-
 .../axis2/openapi/SwaggerUIIntegrityTest.java      | 106 +++++++++++++++++++++
 .../apache/axis2/transport/http/AxisServlet.java   |  28 +++++-
 src/site/markdown/release-notes/2.0.2.md           |  16 ++++
 5 files changed, 238 insertions(+), 6 deletions(-)

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 a9e4ad1cea..8f0787776c 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
@@ -144,6 +144,14 @@ public class OpenApiConfiguration {
     /** Swagger UI version to use */
     private String swaggerUiVersion = "5.32.13";
 
+    /**
+     * Subresource Integrity hashes for the Swagger UI scripts, when the 
deployment
+     * pins a version other than the one this release ships hashes for. A 
version
+     * pin alone says which release to fetch, not that what arrived is that 
release.
+     */
+    private String swaggerUiBundleIntegrity;
+    private String swaggerUiStandalonePresetIntegrity;
+
     /** Maven group and artifact for Swagger UI */
     private String swaggerUiMavenGroupAndArtifact = "org.webjars:swagger-ui";
 
@@ -267,6 +275,10 @@ public class OpenApiConfiguration {
 
         // Swagger UI
         swaggerUiVersion = getProperty(props, "openapi.swaggerUi.version", 
swaggerUiVersion);
+        swaggerUiBundleIntegrity = getProperty(props,
+                "openapi.swaggerUi.integrity.bundle", 
swaggerUiBundleIntegrity);
+        swaggerUiStandalonePresetIntegrity = getProperty(props,
+                "openapi.swaggerUi.integrity.standalonePreset", 
swaggerUiStandalonePresetIntegrity);
 
         // Resource packages (comma-separated)
         String packages = getProperty(props, "openapi.resourcePackages", null);
@@ -428,6 +440,16 @@ public class OpenApiConfiguration {
     public String getSwaggerUiVersion() { return swaggerUiVersion; }
     public void setSwaggerUiVersion(String swaggerUiVersion) { 
this.swaggerUiVersion = swaggerUiVersion; }
 
+    public String getSwaggerUiBundleIntegrity() { return 
swaggerUiBundleIntegrity; }
+    public void setSwaggerUiBundleIntegrity(String swaggerUiBundleIntegrity) {
+        this.swaggerUiBundleIntegrity = swaggerUiBundleIntegrity;
+    }
+
+    public String getSwaggerUiStandalonePresetIntegrity() { return 
swaggerUiStandalonePresetIntegrity; }
+    public void setSwaggerUiStandalonePresetIntegrity(String 
swaggerUiStandalonePresetIntegrity) {
+        this.swaggerUiStandalonePresetIntegrity = 
swaggerUiStandalonePresetIntegrity;
+    }
+
     public String getSwaggerUiMavenGroupAndArtifact() { return 
swaggerUiMavenGroupAndArtifact; }
     public void setSwaggerUiMavenGroupAndArtifact(String 
swaggerUiMavenGroupAndArtifact) {
         this.swaggerUiMavenGroupAndArtifact = swaggerUiMavenGroupAndArtifact;
@@ -523,6 +545,8 @@ public class OpenApiConfiguration {
         copy.scannerClass = this.scannerClass;
         copy.supportSwaggerUi = this.supportSwaggerUi;
         copy.swaggerUiVersion = this.swaggerUiVersion;
+        copy.swaggerUiBundleIntegrity = this.swaggerUiBundleIntegrity;
+        copy.swaggerUiStandalonePresetIntegrity = 
this.swaggerUiStandalonePresetIntegrity;
         copy.swaggerUiMavenGroupAndArtifact = 
this.swaggerUiMavenGroupAndArtifact;
         copy.configLocation = this.configLocation;
         copy.propertiesLocation = this.propertiesLocation;
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 428c8cd1bd..80b853e538 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
@@ -63,6 +63,25 @@ public class SwaggerUIHandler {
     // Default Swagger UI version (can be overridden by configuration)
     private static final String DEFAULT_SWAGGER_UI_VERSION = "5.32.13";
 
+    /**
+     * Subresource Integrity hashes for {@link #DEFAULT_SWAGGER_UI_VERSION}.
+     * <p>
+     * Pinning the version in the URL says which release to ask the CDN for. 
It does
+     * not say that what came back is that release: the page is built to drive
+     * authenticated API calls, so a CDN serving modified script runs it with 
the
+     * reader's session. These are SHA-384 digests of the published 5.32.13 
files,
+     * taken from two independent CDNs and confirmed byte-identical.
+     * <p>
+     * They are version-specific. A deployment pinning a different version 
supplies
+     * its own with {@code openapi.swaggerUi.integrity.bundle} and
+     * {@code openapi.swaggerUi.integrity.standalonePreset}; without them the 
page
+     * still renders, and a warning says integrity is not being enforced.
+     */
+    private static final String DEFAULT_BUNDLE_INTEGRITY =
+            
"sha384-PsJla434CobCNv3y1K4wRavOqkUAvwGEQEfbUmI98CCqqGCJsmuDsgIjM6ZQQODP";
+    private static final String DEFAULT_STANDALONE_PRESET_INTEGRITY =
+            
"sha384-IxiIENmaFuZnXUo3BucU89aVUoQ3Z6o2mk+SzTkxlsiYX23mun5v8eb2wbhsfEr8";
+
     // Default resource paths
     private static final String SWAGGER_UI_ROOT = "/swagger-ui/";
     private static final String API_DOCS_PATH = "/api-docs/";
@@ -330,9 +349,17 @@ public class SwaggerUIHandler {
 
         // Add Swagger UI scripts
         html.append("    <script src=\"https://unpkg.com/swagger-ui-dist@";)
-            
.append(swaggerUiVersion).append("/swagger-ui-bundle.js\"></script>\n")
+            .append(swaggerUiVersion).append("/swagger-ui-bundle.js\"")
+            .append(integrityAttributes(swaggerUiVersion,
+                    configuration.getSwaggerUiBundleIntegrity(),
+                    DEFAULT_BUNDLE_INTEGRITY, "swagger-ui-bundle.js"))
+            .append("></script>\n")
             .append("    <script src=\"https://unpkg.com/swagger-ui-dist@";)
-            
.append(swaggerUiVersion).append("/swagger-ui-standalone-preset.js\"></script>\n");
+            
.append(swaggerUiVersion).append("/swagger-ui-standalone-preset.js\"")
+            .append(integrityAttributes(swaggerUiVersion,
+                    configuration.getSwaggerUiStandalonePresetIntegrity(),
+                    DEFAULT_STANDALONE_PRESET_INTEGRITY, 
"swagger-ui-standalone-preset.js"))
+            .append("></script>\n");
 
         // Add Swagger UI initialization script
         html.append(generateSwaggerUIScript(openApiUrl, scriptNonce));
@@ -489,12 +516,45 @@ public class SwaggerUIHandler {
         return Base64.getEncoder().encodeToString(nonce);
     }
 
+    /**
+     * Builds the integrity and crossorigin attributes for a CDN script tag.
+     *
+     * @param version    the Swagger UI version being requested
+     * @param configured an operator-supplied hash, used whatever the version
+     * @param shipped    the hash this release knows, valid only for the 
default version
+     * @param fileName   named in the warning when no hash applies
+     * @return the attributes to add, or an empty string when no hash is known
+     */
+    private String integrityAttributes(String version, String configured,
+                                       String shipped, String fileName) {
+        String integrity = null;
+        if (configured != null && !configured.trim().isEmpty()) {
+            integrity = configured.trim();
+        } else if (DEFAULT_SWAGGER_UI_VERSION.equals(version)) {
+            integrity = shipped;
+        }
+        if (integrity == null) {
+            // Not fatal: refusing to render would turn a supply-chain 
hardening
+            // into an outage for anyone who pinned a different version.
+            log.warn("Serving " + fileName + " for Swagger UI version " + 
version
+                    + " without Subresource Integrity: this release ships a 
hash for "
+                    + DEFAULT_SWAGGER_UI_VERSION + " only. Set"
+                    + " openapi.swaggerUi.integrity.bundle and"
+                    + " openapi.swaggerUi.integrity.standalonePreset to pin 
this version.");
+            return "";
+        }
+        // crossorigin is required for the browser to check integrity 
cross-origin.
+        return " integrity=\"" + integrity + "\" crossorigin=\"anonymous\"";
+    }
+
     /**
      * 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
+     * <p>Only the nonced initialisation script and the Swagger UI 
distribution may
+     * execute, so script injected anywhere into this page does not run even 
if an
+     * encoding defect is reintroduced. The policy names the CDN origin, which 
says
+     * where script may come from but not what it is; that is what the 
Subresource
+     * Integrity hashes on the script tags are for. Operator-configured
      * custom CSS and JavaScript origins are added to the policy so that
      * customised deployments keep working.
      */
diff --git 
a/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
new file mode 100644
index 0000000000..6f54fdb27e
--- /dev/null
+++ 
b/modules/openapi/src/test/java/org/apache/axis2/openapi/SwaggerUIIntegrityTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.lang.reflect.Method;
+
+import org.apache.axis2.context.ConfigurationContext;
+import org.apache.axis2.context.ConfigurationContextFactory;
+
+import junit.framework.TestCase;
+
+/**
+ * The Swagger UI page is built to drive authenticated API calls, and its 
scripts come
+ * from a CDN. Pinning the version in the URL says which release to ask for, 
not that
+ * what came back is that release, so the script tags carry Subresource 
Integrity
+ * hashes. They are version-specific: a deployment pinning a different version 
has to
+ * supply its own, and the page still renders without them rather than going 
down.
+ */
+public class SwaggerUIIntegrityTest extends TestCase {
+
+    private String attributesFor(OpenApiConfiguration configuration, String 
version,
+                                 String configured, String shipped) throws 
Exception {
+        ConfigurationContext configContext =
+                ConfigurationContextFactory.createEmptyConfigurationContext();
+        SwaggerUIHandler handler = new SwaggerUIHandler(configContext, 
configuration);
+        Method method = 
SwaggerUIHandler.class.getDeclaredMethod("integrityAttributes",
+                String.class, String.class, String.class, String.class);
+        method.setAccessible(true);
+        return (String) method.invoke(handler, version, configured, shipped,
+                "swagger-ui-bundle.js");
+    }
+
+    public void testTheShippedHashIsUsedForTheDefaultVersion() throws 
Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes = attributesFor(configuration,
+                configuration.getSwaggerUiVersion(), null, "sha384-SHIPPED");
+        assertTrue("the shipped hash should be applied, was: " + attributes,
+                attributes.contains("integrity=\"sha384-SHIPPED\""));
+        assertTrue("the browser only checks integrity cross-origin with this",
+                attributes.contains("crossorigin=\"anonymous\""));
+    }
+
+    /** A hash for one version says nothing about another, so it must not be 
reused. */
+    public void testTheShippedHashIsNotAppliedToAnotherVersion() throws 
Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes = attributesFor(configuration, "4.15.5", null, 
"sha384-SHIPPED");
+        assertEquals("a hash from a different version would just break the 
page",
+                "", attributes);
+    }
+
+    /** Which is why an operator pinning a version can supply their own. */
+    public void testAnOperatorSuppliedHashIsUsedForAnyVersion() throws 
Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes =
+                attributesFor(configuration, "4.15.5", "sha384-OPERATOR", 
"sha384-SHIPPED");
+        assertTrue("was: " + attributes, 
attributes.contains("integrity=\"sha384-OPERATOR\""));
+        assertTrue(attributes.contains("crossorigin=\"anonymous\""));
+    }
+
+    public void testAnOperatorHashWinsOverTheShippedOne() throws Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes = attributesFor(configuration,
+                configuration.getSwaggerUiVersion(), "sha384-OPERATOR", 
"sha384-SHIPPED");
+        assertTrue(attributes.contains("sha384-OPERATOR"));
+        assertFalse(attributes.contains("sha384-SHIPPED"));
+    }
+
+    public void testABlankOperatorHashFallsBackRatherThanEmittingNothing() 
throws Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes = attributesFor(configuration,
+                configuration.getSwaggerUiVersion(), "   ", "sha384-SHIPPED");
+        assertTrue(attributes.contains("sha384-SHIPPED"));
+    }
+
+    /** The hashes shipped for the default version must actually be present. */
+    public void testTheDefaultVersionShipsRealHashes() throws Exception {
+        OpenApiConfiguration configuration = new OpenApiConfiguration();
+        String attributes = attributesFor(configuration,
+                configuration.getSwaggerUiVersion(), null,
+                readShippedHash("DEFAULT_BUNDLE_INTEGRITY"));
+        assertTrue("a sha384 SRI hash is expected, was: " + attributes,
+                attributes.contains("integrity=\"sha384-"));
+    }
+
+    private String readShippedHash(String fieldName) throws Exception {
+        java.lang.reflect.Field field = 
SwaggerUIHandler.class.getDeclaredField(fieldName);
+        field.setAccessible(true);
+        return (String) field.get(null);
+    }
+}
diff --git 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
index 7faf7b5ebf..c34bc13f2c 100644
--- 
a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
+++ 
b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/AxisServlet.java
@@ -241,7 +241,8 @@ public class AxisServlet extends HttpServlet {
                             }
                         }
                     }
-                    handleFault(msgContext, out, new AxisFault(t.toString(), 
t));
+                    handleFault(msgContext, out,
+                            new AxisFault(unexpectedErrorReason(msgContext, 
t), t));
                 } catch (AxisFault e2) {
                     log.info(e2);
                     throw new ServletException(e2);
@@ -483,6 +484,31 @@ public class AxisServlet extends HttpServlet {
         }
     }
 
+    /**
+     * The fault reason sent to the caller for an otherwise unhandled 
Throwable.
+     * <p>
+     * {@code Throwable.toString()} is {@code fully.qualified.Class: message}, 
and the
+     * reason goes to the client, so the framework's internals were disclosed 
whatever
+     * {@code sendStacktraceDetailsWithFaults} said. This catch-all serves 
every
+     * request the servlet handles, so that applied to JSON and REST responses 
as much
+     * as to SOAP faults. The Throwable is logged with its stack above either 
way, so
+     * a generic reason costs the operator nothing.
+     *
+     * @param msgContext the message being answered, may be null
+     * @param t          the unhandled error
+     * @return the class name and message when details are enabled, otherwise 
generic
+     */
+    private String unexpectedErrorReason(MessageContext msgContext, Throwable 
t) {
+        if (msgContext != null) {
+            Parameter param = msgContext.getParameter(
+                    
Constants.Configuration.SEND_STACKTRACE_DETAILS_WITH_FAULTS);
+            if (param != null && JavaUtils.isTrue(param.getValue())) {
+                return t.toString();
+            }
+        }
+        return "Internal server error";
+    }
+
     protected void handleFault(MessageContext msgContext, OutputStream out, 
AxisFault e)
             throws AxisFault {
         msgContext.setProperty(MessageContext.TRANSPORT_OUT, out);
diff --git a/src/site/markdown/release-notes/2.0.2.md 
b/src/site/markdown/release-notes/2.0.2.md
index 0dab023f84..2a2e0f835e 100644
--- a/src/site/markdown/release-notes/2.0.2.md
+++ b/src/site/markdown/release-notes/2.0.2.md
@@ -148,6 +148,22 @@ in `SECURITY.md`.
   from the network; if you persist or replicate contexts, restrict the stream 
you
   own.
 
+- **Swagger UI scripts are integrity-pinned.** The page loads swagger-ui from 
a CDN
+  at a pinned version, which says which release to ask for but not that what 
came
+  back is that release -- and the page is built to drive authenticated API 
calls. The
+  script tags now carry Subresource Integrity hashes and 
`crossorigin="anonymous"`.
+  The hashes are version-specific, so a deployment pinning a different Swagger 
UI
+  version supplies its own with `openapi.swaggerUi.integrity.bundle` and
+  `openapi.swaggerUi.integrity.standalonePreset`; without them the page still 
renders
+  and a warning says integrity is not being enforced.
+
+- **Unexpected-error faults no longer name the exception class.** An otherwise
+  unhandled error produced a fault reason of `fully.qualified.Class: message`
+  whatever `sendStacktraceDetailsWithFaults` was set to, and since that 
catch-all
+  serves every request the servlet handles, it applied to JSON and REST 
responses as
+  much as to SOAP faults. The reason is now generic unless details are 
enabled. The
+  error is logged with its stack as before.
+
 - **OpenAPI and Swagger UI output.** Request-controlled values are validated 
and
   encoded for the context they are written into, the served page carries a
   Content-Security-Policy with a per-response script nonce, and the published

Reply via email to