From 01216053db290dbcc904017c8ccd00cb4ba45090 Mon Sep 17 00:00:00 2001 From: Yannick Marcon Date: Sun, 22 Dec 2024 17:58:34 +0100 Subject: [PATCH] fix: ensure Referer header is provided or check allowed user agent --- .../opal/web/security/CSRFInterceptor.java | 43 +++++++++++-------- .../resources/META-INF/defaults.properties | 4 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/opal-core-ws/src/main/java/org/obiba/opal/web/security/CSRFInterceptor.java b/opal-core-ws/src/main/java/org/obiba/opal/web/security/CSRFInterceptor.java index 9cdb8c1b97..50c04ba153 100644 --- a/opal-core-ws/src/main/java/org/obiba/opal/web/security/CSRFInterceptor.java +++ b/opal-core-ws/src/main/java/org/obiba/opal/web/security/CSRFInterceptor.java @@ -40,28 +40,36 @@ public class CSRFInterceptor extends AbstractSecurityComponent implements Reques private static final String REFERER_HEADER = "Referer"; + private static final String USER_AGENT_HEADER = "User-Agent"; + private static final Pattern localhostPattern = Pattern.compile("^http[s]?://localhost:.*"); private static final Pattern loopbackhostPattern = Pattern.compile("^http[s]?://127\\.0\\.0\\.1:.*"); private final boolean productionMode; - private final List csrfAllowed; + private final List csrfAllowedHosts; + + private final List csrfAllowedAgents; @Autowired public CSRFInterceptor(@Value("${productionMode}") boolean productionMode, - @Value("${csrf.allowed}") String csrfAllowed) { + @Value("${csrf.allowed}") String csrfAllowedHosts, + @Value("${csrf.allowed-agents}") String csrfAllowedAgents) { this.productionMode = productionMode; - this.csrfAllowed = Strings.isNullOrEmpty(csrfAllowed) ? Lists.newArrayList() : Splitter.on(",").splitToList(csrfAllowed.trim()); + this.csrfAllowedHosts = Strings.isNullOrEmpty(csrfAllowedHosts) ? Lists.newArrayList() : Splitter.on(",").splitToList(csrfAllowedHosts.trim()); + this.csrfAllowedAgents = Strings.isNullOrEmpty(csrfAllowedAgents) ? Lists.newArrayList() : Splitter.on(",").splitToList(csrfAllowedAgents.trim()); } @Override public void preProcess(HttpServletRequest httpServletRequest, ResourceMethodInvoker resourceMethod, ContainerRequestContext requestContext) { - if (!productionMode || csrfAllowed.contains("*")) return; + if (!productionMode || csrfAllowedHosts.contains("*")) return; String host = requestContext.getHeaderString(HOST_HEADER); + if (matchesLocalhost(host)) return; + String referer = requestContext.getHeaderString(REFERER_HEADER); - if (referer != null) { + if (!Strings.isNullOrEmpty(referer)) { String refererHostPort = ""; try { URI refererURI = URI.create(referer); @@ -70,20 +78,22 @@ public void preProcess(HttpServletRequest httpServletRequest, ResourceMethodInvo // malformed url } // explicitly ok - if (csrfAllowed.contains(refererHostPort)) return; - - boolean forbidden = false; - if (!matchesLocalhost(host) && !referer.startsWith(String.format("https://%s/", host))) { - forbidden = true; - } + if (csrfAllowedHosts.contains(refererHostPort)) return; + boolean forbidden = !referer.startsWith(String.format("https://%s/", host)); if (forbidden) { log.warn("CSRF detection: Host={}, Referer={}", host, referer); log.info(">> You can add {} to csrf.allowed setting", refererHostPort); throw new ForbiddenException("CSRF error"); } + } else { + String userAgent = requestContext.getHeaderString(USER_AGENT_HEADER); + if (Strings.isNullOrEmpty(userAgent) || !matchesUserAgent(userAgent)) { + log.warn("CSRF detection: Host={}, User-Agent={}", host, userAgent); + log.info(">> Ensure 'Referer' HTTP header is set or allow this 'User-Agent' with 'csrf.allowed-agents' setting"); + throw new ForbiddenException("CSRF error"); + } } - return; } private boolean matchesLocalhost(String host) { @@ -93,13 +103,8 @@ private boolean matchesLocalhost(String host) { || host.startsWith("127.0.0.1:"); } - static String asHeader(Iterable values) { - StringBuilder sb = new StringBuilder(); - for (String s : values) { - if (!sb.isEmpty()) sb.append(", "); - sb.append(s); - } - return sb.toString(); + private boolean matchesUserAgent(String userAgent) { + return csrfAllowedAgents.stream().anyMatch(ua -> userAgent.toLowerCase().contains(ua.toLowerCase())); } } diff --git a/opal-core/src/main/resources/META-INF/defaults.properties b/opal-core/src/main/resources/META-INF/defaults.properties index b3ae1b5cf3..61c3c609ff 100644 --- a/opal-core/src/main/resources/META-INF/defaults.properties +++ b/opal-core/src/main/resources/META-INF/defaults.properties @@ -73,8 +73,10 @@ apps.registration.exclude= apps.discovery.interval = 10000 # CSRF -# allowed referers, comma separated +# allowed referrers, comma separated csrf.allowed= +# allowed user agents when referrer is not specified +csrf.allowed-agents=curl,python # CORS # use * as wildcard, separate origins with commas