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 a483b5cdfdf544587583ba77855a9b91bcdbc420 Author: Robert Lazarski <[email protected]> AuthorDate: Tue Aug 4 14:26:34 2026 -1000 Publish a relative OpenAPI server URL instead of following the Host Closes F3 by default rather than leaving it to configuration. The earlier position -- that a relative servers[].url risked breaking MCP clients that need an absolute one -- does not survive contact with the actual consumers. Axis2's own MCP catalog never emits servers[] or any absolute base URL; generateMcpCatalogJson publishes relative paths ("POST /services/...") and a relative tokenEndpoint, so /openapi-mcp.json was never affected by this at all. The claim in the report that MCP tool clients use servers[].url does not hold for this implementation. Nor do the JSON and OpenAPI clients this surface was designed against. Those surveyed carry no MCP code, do not fetch the specification at runtime -- one bundles a snapshot of it -- and resolve their base URL from configuration. None of them reads servers[].url. So servers[].url is now the context path. OpenAPI 3 resolves that against wherever the document was fetched from, which is both safer -- the URL a consumer sends credentials to is no longer chosen by whoever set the Host -- and more accurate behind a proxy. openapi.serverBaseUrl still pins an absolute URL where one is wanted. The HTTP/2 tests asserted the spec advertised an https:// server; a relative URL inherits the document's scheme, so that premise is obsolete and they now assert relativity instead. Co-Authored-By: Claude Fable 5 <[email protected]> --- .../apache/axis2/openapi/OpenApiSpecGenerator.java | 62 ++++++---------------- .../axis2/openapi/Http2OpenApiBasicTest.java | 20 ++++--- .../axis2/openapi/Http2OpenApiIntegrationTest.java | 20 ++++--- .../axis2/openapi/OpenApiSpecGeneratorTest.java | 46 +++++++++------- 4 files changed, 69 insertions(+), 79 deletions(-) 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 03b5d2a785..ad732e8b3c 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 @@ -242,53 +242,25 @@ public class OpenApiSpecGenerator { 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); - - // Add port if not default - if ((scheme.equals("http") && serverPort != 80) || - (scheme.equals("https") && serverPort != 443)) { - serverUrl.append(":").append(serverPort); - } - - serverUrl.append(contextPath); - - Server server = new Server(); - server.setUrl(serverUrl.toString()); - server.setDescription("Current server"); - servers.add(server); - } else { - // Default server - Server server = new Server(); - server.setUrl("http://localhost:8080"); - server.setDescription("Default server"); - servers.add(server); + // Otherwise publish a relative server URL. OpenAPI 3 resolves it against + // the location the document was retrieved from, which is already the + // right origin for any client that just fetched this specification. + // + // Deriving it from the request Host instead would mean the URL every + // consumer sends its next request to — carrying whatever credentials it + // holds — is chosen by whoever set that header. A relative URL cannot be + // pointed anywhere, and it is also simply more accurate behind a proxy, + // where the Host the container sees is often not the public origin. + String contextPath = request == null ? null : request.getContextPath(); + if (contextPath == null || contextPath.isEmpty()) { + contextPath = "/"; } + Server server = new Server(); + server.setUrl(contextPath); + server.setDescription("Current server"); + servers.add(server); + return servers; } diff --git a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java index 0b56aa776d..3bcb818d25 100644 --- a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java +++ b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiBasicTest.java @@ -96,14 +96,15 @@ public class Http2OpenApiBasicTest extends TestCase { assertNotNull("Should have servers", servers); assertFalse("Should have at least one server", servers.isEmpty()); - boolean hasHttpsServer = servers.stream() - .anyMatch(server -> server.getUrl().startsWith("https://")); - assertTrue("Should have HTTPS server for HTTP/2 compatibility", hasHttpsServer); + // The server URL is relative, so a client resolves it against the + // origin it fetched the document from. Over HTTPS that yields HTTPS + // requests without the spec having to name a scheme at all. + boolean allRelative = servers.stream() + .allMatch(server -> server.getUrl().startsWith("/")); + assertTrue("Server URL should be scheme-relative for HTTP/2 compatibility", allRelative); System.out.println("✅ HTTPS OpenAPI spec generation: PASSED"); System.out.println(" - Generated spec with " + openApi.getPaths().size() + " paths"); - System.out.println(" - HTTPS servers configured: " + - servers.stream().filter(s -> s.getUrl().startsWith("https://")).count()); } /** @@ -242,9 +243,12 @@ public class Http2OpenApiBasicTest extends TestCase { // Verify HTTPS servers are configured for HTTP/2 compatibility List<Server> servers = openApi.getServers(); assertNotNull("Should have servers", servers); - boolean hasHttpsServer = servers.stream() - .anyMatch(server -> server.getUrl().startsWith("https://")); - assertTrue("Should have HTTPS server for HTTP/2 compatibility", hasHttpsServer); + // The server URL is relative, so a client resolves it against the + // origin it fetched the document from. Over HTTPS that yields HTTPS + // requests without the spec having to name a scheme at all. + boolean allRelative = servers.stream() + .allMatch(server -> server.getUrl().startsWith("/")); + assertTrue("Server URL should be scheme-relative for HTTP/2 compatibility", allRelative); System.out.println("✅ HTTP/2 compatibility validation: PASSED"); System.out.println(" - HTTPS requirement satisfied"); diff --git a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java index 7af06be945..b3cffd9470 100644 --- a/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java +++ b/modules/openapi/src/test/java/org/apache/axis2/openapi/Http2OpenApiIntegrationTest.java @@ -108,10 +108,13 @@ public class Http2OpenApiIntegrationTest extends TestCase { assertNotNull("Should have servers configured", servers); assertFalse("Should have at least one server", servers.isEmpty()); - // Verify HTTPS protocol (required for HTTP/2) - boolean hasHttpsServer = servers.stream() - .anyMatch(server -> server.getUrl().startsWith("https://")); - assertTrue("Should have HTTPS server for HTTP/2 compatibility", hasHttpsServer); + // Verify the scheme is inherited rather than pinned (HTTP/2 needs HTTPS) + // The server URL is relative, so a client resolves it against the + // origin it fetched the document from. Over HTTPS that yields HTTPS + // requests without the spec having to name a scheme at all. + boolean allRelative = servers.stream() + .allMatch(server -> server.getUrl().startsWith("/")); + assertTrue("Server URL should be scheme-relative for HTTP/2 compatibility", allRelative); // Verify services are properly documented assertTrue("Should have documented endpoints", openApi.getPaths().size() > 0); @@ -357,9 +360,12 @@ public class Http2OpenApiIntegrationTest extends TestCase { // Validate HTTPS configuration List<Server> servers = secureApi.getServers(); - boolean hasSecureServer = servers.stream() - .anyMatch(server -> server.getUrl().startsWith("https://")); - assertTrue("Should require HTTPS for HTTP/2", hasSecureServer); + // The server URL is relative, so a client resolves it against the + // origin it fetched the document from. Over HTTPS that yields HTTPS + // requests without the spec having to name a scheme at all. + boolean allRelative = servers.stream() + .allMatch(server -> server.getUrl().startsWith("/")); + assertTrue("Server URL should be scheme-relative for HTTP/2", allRelative); // Validate security schemes if (secureApi.getComponents() != null && secureApi.getComponents().getSecuritySchemes() != null) { diff --git a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java index 57901b9e22..c8ef81a532 100644 --- a/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java +++ b/modules/openapi/src/test/java/org/apache/axis2/openapi/OpenApiSpecGeneratorTest.java @@ -103,8 +103,8 @@ public class OpenApiSpecGeneratorTest extends TestCase { } /** - * Test server list generation from HTTP request. - * Verifies correct server URL construction. + * The published server URL is relative to where the document was fetched + * from, so it does not follow the client-supplied Host. */ public void testServerListGeneration() throws Exception { // Arrange @@ -122,14 +122,11 @@ public class OpenApiSpecGeneratorTest extends TestCase { assertFalse("At least one server should be configured", servers.isEmpty()); Server server = servers.get(0); - assertEquals("https://api.example.com:8443/axis2", server.getUrl()); + assertEquals("/axis2", server.getUrl()); assertEquals("Current server", server.getDescription()); } - /** - * Test server list generation with default HTTP port. - * Should not include port 80 in URL. - */ + /** A deployment at the root context publishes "/". */ public void testServerListGenerationDefaultHttpPort() throws Exception { // Arrange mockRequest.setScheme("http"); @@ -142,13 +139,10 @@ public class OpenApiSpecGeneratorTest extends TestCase { // Assert Server server = openApi.getServers().get(0); - assertEquals("http://localhost", server.getUrl()); + assertEquals("/", server.getUrl()); } - /** - * Test server list generation with default HTTPS port. - * Should not include port 443 in URL. - */ + /** The scheme and port no longer take part in the published URL. */ public void testServerListGenerationDefaultHttpsPort() throws Exception { // Arrange mockRequest.setScheme("https"); @@ -161,13 +155,10 @@ public class OpenApiSpecGeneratorTest extends TestCase { // Assert Server server = openApi.getServers().get(0); - assertEquals("https://secure.example.com/api", server.getUrl()); + assertEquals("/api", server.getUrl()); } - /** - * Test OpenAPI generation with null request. - * Should use default server configuration. - */ + /** With no request there is no context path either, so the root is used. */ public void testOpenApiGenerationWithNullRequest() throws Exception { // Act OpenAPI openApi = generator.generateOpenApiSpec(null); @@ -179,8 +170,25 @@ public class OpenApiSpecGeneratorTest extends TestCase { assertFalse("Should have at least one server", servers.isEmpty()); Server server = servers.get(0); - assertEquals("http://localhost:8080", server.getUrl()); - assertEquals("Default server", server.getDescription()); + assertEquals("/", server.getUrl()); + assertEquals("Current server", server.getDescription()); + } + + /** + * An operator who needs an absolute URL published - behind a proxy, say - + * pins it explicitly rather than having it inferred from the request. + */ + public void testConfiguredServerBaseUrlIsPublished() throws Exception { + OpenApiConfiguration configuration = new OpenApiConfiguration(); + configuration.setServerBaseUrl("https://api.example.com/axis2"); + OpenApiSpecGenerator pinned = + new OpenApiSpecGenerator(configurationContext, configuration); + + mockRequest.setServerName("attacker.evil.example"); + Server server = pinned.generateOpenApiSpec(mockRequest).getServers().get(0); + + assertEquals("https://api.example.com/axis2", server.getUrl()); + assertEquals("Configured server", server.getDescription()); } /**
