From 3877e914c49571afaab71d1ed1d9dd6a1cbf9617 Mon Sep 17 00:00:00 2001 From: Alexander Skoblikov Date: Tue, 26 Aug 2025 16:01:25 +0300 Subject: [PATCH] dbeaver/pro#6462 error instead of redirect for gql requests (#3690) * dbeaver/pro#6462 error instead of redirect for gql requests * feat: force redirect on supportedHosts change * fix: replace redirect with warning message * fix: prevent active host removal * fix: error message and ignore when on IP address --------- Co-authored-by: Alexey Potsetsuev Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com> --- .../server/jetty/CBJettyServer.java | 9 ++- .../server/jetty/RequestHostFilter.java | 57 ++++++++++++++----- .../core-blocks/src/FormControls/Textarea.tsx | 8 ++- .../FormControls/useCustomInputValidation.ts | 16 +++--- webapp/packages/core-utils/src/index.ts | 1 + webapp/packages/core-utils/src/isIp.ts | 17 ++++++ ...onfigurationWizardPagesBootstrapService.ts | 2 +- .../Form/ServerConfigurationInfoForm.tsx | 24 +++++++- .../ServerConfigurationFormPart.ts | 19 ++++++- .../ServerConfigurationFormStateManager.ts | 13 ++++- .../ServerConfigurationPage.tsx | 2 +- .../getServerConfigurationFormPart.ts | 5 +- .../plugin-administration/src/locales/en.ts | 4 ++ .../plugin-administration/src/locales/fr.ts | 4 ++ .../plugin-administration/src/locales/it.ts | 4 ++ .../plugin-administration/src/locales/ru.ts | 4 ++ .../plugin-administration/src/locales/vi.ts | 4 ++ .../plugin-administration/src/locales/zh.ts | 4 ++ 18 files changed, 165 insertions(+), 32 deletions(-) create mode 100644 webapp/packages/core-utils/src/isIp.ts diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 10f68c0e32..4db964b82b 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -120,12 +120,13 @@ public class CBJettyServer { GraphQLEndpoint endpoint = new GraphQLEndpoint(new ServerConfigurationTimeLimitFilter(application)); application.addApplicationContextValue(GraphQL.class.getName(), endpoint.getGraphQL()); + String gqlServletPath = serverConfiguration.getServicesURI() + "gql/*"; servletContextHandler.addServlet( new ServletHolder( "graphql", endpoint ), - serverConfiguration.getServicesURI() + "gql/*" + gqlServletPath ); servletContextHandler.addEventListener(new CBServerContextListener(application)); @@ -157,7 +158,11 @@ public class CBJettyServer { } } } - FilterHolder hostsFilter = new FilterHolder(new RequestHostFilter(application, excludedFilterPaths)); + FilterHolder hostsFilter = new FilterHolder(new RequestHostFilter( + application, + excludedFilterPaths, + Set.of(gqlServletPath) + )); servletContextHandler.addFilter(hostsFilter, "/*", null); diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java index f2406ddb4b..2666a3f76d 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/jetty/RequestHostFilter.java @@ -39,18 +39,30 @@ public class RequestHostFilter implements Filter { @NotNull private final CBApplication application; private final Set excludedPaths = new HashSet<>(); + private final Set errorPaths = new HashSet<>(); - public RequestHostFilter(@NotNull CBApplication application, @NotNull Set excludedPaths) { + public RequestHostFilter( + @NotNull CBApplication application, + @NotNull Set excludedPaths, + @NotNull Set errorPaths + ) { this.application = application; this.excludedPaths.addAll( excludedPaths.stream() .map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", ""))) .toList() ); + this.errorPaths.addAll( + errorPaths.stream() + .map(path -> ServletAppUtils.removeSideSlashes(path.replace("*", ""))) + .toList() + ); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + boolean requestAllowed = true; + if (request instanceof HttpServletRequest httpRequest) { CBServerConfig serverConfig = application.getServerConfiguration(); URI originUri; @@ -64,7 +76,8 @@ public class RequestHostFilter implements Filter { } boolean isIpAddress = InetAddresses.isInetAddress(originUri.getHost()); String servletPath = httpRequest.getServletPath(); - if (!isIpAddress) { + requestAllowed = isIpAddress; + if (!requestAllowed) { if (CommonUtils.isNotEmpty(servletPath)) { for (String excludedPath : excludedPaths) { if (servletPath.contains(excludedPath)) { @@ -73,17 +86,21 @@ public class RequestHostFilter implements Filter { } } } - validateHosts(serverConfig, httpRequest, response, originUri); + requestAllowed = validateHosts(serverConfig, httpRequest, (HttpServletResponse) response, originUri); + } + if (requestAllowed) { + requestAllowed = validateSchema(serverConfig, httpRequest, (HttpServletResponse) response, originUri); } - validateSchema(serverConfig, httpRequest, response, originUri); } - chain.doFilter(request, response); + if (requestAllowed) { + chain.doFilter(request, response); + } } - private void validateSchema( + private boolean validateSchema( @NotNull CBServerConfig serverConfig, @NotNull HttpServletRequest httpRequest, - @NotNull ServletResponse response, + @NotNull HttpServletResponse response, @NotNull URI originUri ) { boolean httpsExpected = serverConfig.isForceHttps(); @@ -100,22 +117,24 @@ public class RequestHostFilter implements Filter { redirectUrlBuilder.append("?") .append(httpRequest.getQueryString()); } - ((HttpServletResponse) response).sendRedirect(redirectUrlBuilder.toString()); + response.sendRedirect(redirectUrlBuilder.toString()); + return false; } } catch (Exception e) { log.error("Failed to redirect to HTTPS", e); } + return true; } - private void validateHosts( + private boolean validateHosts( @NotNull CBServerConfig serverConfig, @NotNull HttpServletRequest httpRequest, - @NotNull ServletResponse response, + @NotNull HttpServletResponse response, URI originUri ) throws IOException { List availableHosts = serverConfig.getSupportedHosts(); if (CommonUtils.isEmpty(availableHosts)) { - return; + return true; } try { var requestHostBuilder = new StringBuilder(originUri.getHost()); @@ -124,13 +143,25 @@ public class RequestHostFilter implements Filter { } String requestHost = requestHostBuilder.toString(); if (!availableHosts.contains(requestHost)) { + for (String errorPath : errorPaths) { + if (httpRequest.getServletPath().contains(errorPath)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write( + "Request host '" + requestHost + "' is not allowed. Available hosts: " + availableHosts + ); + return false; + } + } log.warn("Request host '" + requestHost + "' is not allowed. Redirect to default: " + availableHosts); - redirectToDefaultHost((HttpServletResponse) response, httpRequest, availableHosts); + redirectToDefaultHost(response, httpRequest, availableHosts); + return false; } } catch (Throwable e) { log.error(e.getMessage(), e); - redirectToDefaultHost((HttpServletResponse) response, httpRequest, availableHosts); + redirectToDefaultHost(response, httpRequest, availableHosts); + return false; } + return true; } private void redirectToDefaultHost( diff --git a/webapp/packages/core-blocks/src/FormControls/Textarea.tsx b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx index d7cfe6db6d..ade763c166 100644 --- a/webapp/packages/core-blocks/src/FormControls/Textarea.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx @@ -23,10 +23,12 @@ import { FieldDescription } from './FieldDescription.js'; import { FieldLabel } from './FieldLabel.js'; import { FormContext } from './FormContext.js'; import textareaStyle from './Textarea.module.css'; +import { useMergeRefs } from '../useMergeRefs.js'; type BaseProps = Omit, 'onChange' | 'style'> & ILayoutSizeProps & { - description?: string; + ref?: React.Ref; + description?: React.ReactNode; labelTooltip?: string; embedded?: boolean; cursorInitiallyAtEnd?: boolean; @@ -53,6 +55,7 @@ interface TextareaType { } export const Textarea: TextareaType = observer(function Textarea({ + ref, name, value: controlledValue, state, @@ -71,6 +74,7 @@ export const Textarea: TextareaType = observer(function Textarea({ const translate = useTranslate(); const inputId = useId(); const textareaRef = useRef(null); + const mergedRef = useMergeRefs(...[textareaRef, ref!].filter(Boolean)); const layoutProps = getLayoutProps(rest); rest = filterLayoutFakeProps(rest); const styles = useS(textareaStyle); @@ -108,7 +112,7 @@ export const Textarea: TextareaType = observer(function Textarea({