From 8f6d89343a8c16eee2f0ffcb5a889a68c3506d1c Mon Sep 17 00:00:00 2001 From: Raghav Aggarwal Date: Sun, 19 Jul 2026 17:01:06 +0530 Subject: [PATCH] TEZ-4742: AMWebController CORS: only reflect Origin when it matches configured Tez UI base URL --- .../tez/dag/app/web/AMWebController.java | 35 ++++++++++++------- .../tez/dag/app/web/TestAMWebController.java | 18 +++++++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java b/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java index cf1105bea6..e547dad520 100644 --- a/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java +++ b/tez-dag/src/main/java/org/apache/tez/dag/app/web/AMWebController.java @@ -132,27 +132,36 @@ static String encodeHeader(final String header) { } @VisibleForTesting - public void setCorsHeaders() { - final HttpServletResponse res = response(); - - /* - * ideally the Origin and other CORS headers should be checked and response headers set only - * if it matches the allowed origins. however rm does not forward these headers. - */ + String getTrustedOrigin() { String historyUrlBase = appContext.getAMConf().get(TezConfiguration.TEZ_HISTORY_URL_BASE, ""); - String origin = request().getHeader(ORIGIN); - if(origin == null) { + if (!historyUrlBase.isEmpty()) { try { URL url = URI.create(historyUrlBase).toURL(); - origin = url.getProtocol() + "://" + url.getAuthority(); + return url.getProtocol() + "://" + url.getAuthority(); } catch (IllegalArgumentException | MalformedURLException e) { LOG.debug("Invalid url set for tez history url base: {}", historyUrlBase, e); } } + return null; + } - if (origin != null) { - origin = encodeHeader(origin); - res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + @VisibleForTesting + public void setCorsHeaders() { + final HttpServletResponse res = response(); + String trustedOrigin = getTrustedOrigin(); + String requestOrigin = request().getHeader(ORIGIN); + + // We cannot blindly reflect the request origin in the Access-Control-Allow-Origin header + // because it would allow malicious sites to bypass CORS and access sensitive data, + // effectively defeating the purpose of CORS. We must strictly validate the request origin + // against the configured, trusted Tez UI base URL before reflecting it. + if (trustedOrigin != null) { + if (requestOrigin == null || requestOrigin.equals(trustedOrigin)) { + res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, encodeHeader(trustedOrigin)); + } else { + LOG.debug("CORS validation failed: request origin {} does not match configured Tez UI base URL {}", + requestOrigin, trustedOrigin); + } } res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, ALLOWED_METHODS); res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString()); diff --git a/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java b/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java index ee81b9de83..e5a8c0e0a7 100644 --- a/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java +++ b/tez-dag/src/test/java/org/apache/tez/dag/app/web/TestAMWebController.java @@ -28,8 +28,10 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -121,15 +123,23 @@ public void testCorsHeadersWithOrigin() { AMWebController amWebController = new AMWebController(mockRequestContext, mockAppContext, "TEST_HISTORY_URL"); AMWebController spy = spy(amWebController); - String originURL = "http://origin.com:8080"; + String validOrigin = "http://uihost:9001"; + String invalidOrigin = "http://origin.com:8080"; doReturn(mockResponse).when(spy).response(); - doReturn(mockRequest).when(spy).request(); - doReturn(originURL).when(mockRequest).getHeader(AMWebController.ORIGIN); + // Test 1: Valid origin that matches the configured TEZ_HISTORY_URL_BASE + doReturn(validOrigin).when(mockRequest).getHeader(AMWebController.ORIGIN); + spy.setCorsHeaders(); + verify(mockResponse, times(1)).setHeader("Access-Control-Allow-Origin", validOrigin); + + // Test 2: Invalid origin that does not match + reset(mockResponse); // Reset the mock to clear the previous invocations + doReturn(invalidOrigin).when(mockRequest).getHeader(AMWebController.ORIGIN); spy.setCorsHeaders(); - verify(mockResponse).setHeader("Access-Control-Allow-Origin", originURL); + // The header should NOT be set because the origin is invalid + verify(mockResponse, never()).setHeader(eq("Access-Control-Allow-Origin"), anyString()); } @Test