Skip to content
Draft
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
3 changes: 3 additions & 0 deletions conf/shiro.ini.template
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
cookie = org.apache.shiro.web.servlet.SimpleCookie
cookie.name = JSESSIONID
cookie.httpOnly = true
### Restrict the session cookie to same-site requests by default. Set to NONE only when
### Zeppelin is intentionally embedded into a different origin (and 'cookie.secure = true').
cookie.sameSite = LAX
### Uncomment the below line only when Zeppelin is running over HTTPS
#cookie.secure = true
sessionManager.sessionIdCookie = $cookie
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ public String addParagraph(String noteId, String title, String text) throws Exce
bodyObject.put("text", text);
HttpResponse<JsonNode> response = Unirest.post("/notebook/{noteId}/paragraph")
.routeParam("noteId", noteId)
.header("Content-Type", "application/json")
.body(bodyObject.toString())
.asJson();
checkResponse(response);
Expand All @@ -617,6 +618,7 @@ public void updateParagraph(String noteId, String paragraphId, String title, Str
HttpResponse<JsonNode> response = Unirest.put("/notebook/{noteId}/paragraph/{paragraphId}")
.routeParam("noteId", noteId)
.routeParam("paragraphId", paragraphId)
.header("Content-Type", "application/json")
.body(bodyObject.toString())
.asJson();
checkResponse(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,9 @@ public enum ConfVars {
"https://github.com/yarnpkg/yarn/releases/download/"),
// Allows a way to specify a ',' separated list of allowed origins for rest and websockets
// i.e. http://localhost:8080
ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", "*"),
// Default is empty (no cross-origin requests permitted). Operators that need cross-origin
// access must set this explicitly to the trusted origin(s) or to "*".
ZEPPELIN_ALLOWED_ORIGINS("zeppelin.server.allowed.origins", ""),
ZEPPELIN_USERNAME_FORCE_LOWERCASE("zeppelin.username.force.lowercase", false),
ZEPPELIN_CREDENTIALS_PERSIST("zeppelin.credentials.persist", true),
ZEPPELIN_CREDENTIALS_ENCRYPT_KEY("zeppelin.credentials.encryptKey", null),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.zeppelin.rest.filter;

import java.util.Locale;
import java.util.Set;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;

import org.apache.zeppelin.utils.HttpMethods;

/**
* Restricts the request body media types accepted by REST endpoints to a small allow-list.
* Requests carrying state-changing methods (POST/PUT/DELETE/PATCH) with a body must use
* {@code application/json}, {@code application/x-www-form-urlencoded}, or
* {@code multipart/form-data}; anything else is rejected with 415.
*/
@Provider
public class JsonContentTypeFilter implements ContainerRequestFilter {

private static final Set<String> ALLOWED_TYPES = Set.of(
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data");

@Override
public void filter(ContainerRequestContext ctx) {
String method = ctx.getMethod();
if (method == null || !HttpMethods.STATE_CHANGING.contains(method.toUpperCase(Locale.ROOT))) {
return;
}
if (!ctx.hasEntity()) {
return;
}
MediaType mt = ctx.getMediaType();
if (mt == null || !ALLOWED_TYPES.contains(baseType(mt))) {
ctx.abortWith(
Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE)
.entity("Unsupported Content-Type")
.build());
}
}

private static String baseType(MediaType mt) {
return (mt.getType() + "/" + mt.getSubtype()).toLowerCase(Locale.ROOT);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Locale;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
Expand All @@ -28,6 +29,7 @@
import jakarta.servlet.http.HttpServletResponse;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.utils.CorsUtils;
import org.apache.zeppelin.utils.HttpMethods;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -46,33 +48,52 @@ public CorsFilter(ZeppelinConfiguration zConf) {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {
String sourceHost = ((HttpServletRequest) request).getHeader("Origin");
String origin = "";
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

try {
if (CorsUtils.isValidOrigin(sourceHost, zConf)) {
origin = sourceHost;
String sourceHost = httpRequest.getHeader(CorsUtils.HEADER_ORIGIN);
String method = httpRequest.getMethod();
String allowedOrigin = "";

if (sourceHost != null && !sourceHost.isEmpty()) {
try {
if (CorsUtils.isValidOrigin(sourceHost, zConf)) {
allowedOrigin = sourceHost;
}
} catch (URISyntaxException e) {
LOGGER.warn("Rejecting request with malformed Origin header: {}", sourceHost);
}
} catch (URISyntaxException e) {
LOGGER.error("Exception in WebDriverManager while getWebDriver ", e);
}

if (((HttpServletRequest) request).getMethod().equals("OPTIONS")) {
HttpServletResponse resp = ((HttpServletResponse) response);
addCorsHeaders(resp, origin);
return;
if (allowedOrigin.isEmpty() && (isCorsPreflight(httpRequest) || isStateChanging(method))) {
LOGGER.warn("Blocking cross-origin {} request from disallowed Origin: {}",
method, sourceHost);
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Origin not allowed");
return;
}
}

if (response instanceof HttpServletResponse) {
HttpServletResponse alteredResponse = ((HttpServletResponse) response);
addCorsHeaders(alteredResponse, origin);
addCorsHeaders(httpResponse, allowedOrigin);
if (isCorsPreflight(httpRequest)) {
return;
}
filterChain.doFilter(request, response);
}

private static boolean isCorsPreflight(HttpServletRequest request) {
return "OPTIONS".equalsIgnoreCase(request.getMethod())
&& request.getHeader("Access-Control-Request-Method") != null;
}

private static boolean isStateChanging(String method) {
return method != null
&& HttpMethods.STATE_CHANGING.contains(method.toUpperCase(Locale.ROOT));
}

private void addCorsHeaders(HttpServletResponse response, String origin) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
if (!origin.isEmpty()) {
response.setHeader("Access-Control-Allow-Credentials", "true");
}
response.setHeader("Access-Control-Allow-Headers", "authorization,Content-Type");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, HEAD, DELETE");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.apache.zeppelin.rest.ZeppelinRestApi;
import org.apache.zeppelin.rest.exception.WebApplicationExceptionMapper;
import org.apache.zeppelin.rest.filter.CacheControlFilter;
import org.apache.zeppelin.rest.filter.JsonContentTypeFilter;
import org.glassfish.jersey.server.ServerProperties;

public class RestApiApplication extends Application {
Expand All @@ -60,6 +61,7 @@ public Set<Class<?>> getClasses() {
s.add(GsonProvider.class);
// Filter
s.add(CacheControlFilter.class);
s.add(JsonContentTypeFilter.class);
return s;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Locale;
import org.apache.zeppelin.conf.ZeppelinConfiguration;

public class CorsUtils {
Expand All @@ -36,15 +37,19 @@ public static boolean isValidOrigin(String sourceHost, ZeppelinConfiguration zCo

if (sourceHost != null && !sourceHost.isEmpty()) {
sourceUriHost = new URI(sourceHost).getHost();
sourceUriHost = (sourceUriHost == null) ? "" : sourceUriHost.toLowerCase();
sourceUriHost = (sourceUriHost == null) ? "" : sourceUriHost.toLowerCase(Locale.ROOT);
}

sourceUriHost = sourceUriHost.toLowerCase();
String currentHost = InetAddress.getLocalHost().getHostName().toLowerCase();
String currentHost = InetAddress.getLocalHost().getHostName().toLowerCase(Locale.ROOT);
// getAllowedOrigins() returns lowercased entries; normalize sourceHost the same way
// before the membership check so case differences in the Origin header do not produce
// false rejections of explicitly configured origins.
String normalizedOrigin =
sourceHost == null ? "" : sourceHost.toLowerCase(Locale.ROOT);

return zConf.getAllowedOrigins().contains("*")
|| currentHost.equals(sourceUriHost)
|| "localhost".equals(sourceUriHost)
|| zConf.getAllowedOrigins().contains(sourceHost);
|| zConf.getAllowedOrigins().contains(normalizedOrigin);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.zeppelin.utils;

import java.util.Set;

public final class HttpMethods {

private HttpMethods() {
}

public static final Set<String> STATE_CHANGING = Set.of("POST", "PUT", "DELETE", "PATCH");
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void getAllowedOriginsNoneTest() throws MalformedURLException {

ZeppelinConfiguration zConf = ZeppelinConfiguration.load("zeppelin-test-site.xml");
List<String> origins = zConf.getAllowedOrigins();
assertEquals(1, origins.size());
assertTrue(origins.isEmpty());
}

@Test
Expand Down
Loading
Loading