Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Comment on lines +137 to 144

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe refactor this to a method that derives the origin or returns null

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));
Comment on lines +159 to +160

@abstractdog abstractdog Jul 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make a code comment above this for "educational purposes" regarding why we cannot reflect the request origin without checking the server's trusted origin, as this is the core of this fix

} 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down
Loading