This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new b95b7cd4c6 [SONAR] cleanup security backlog
b95b7cd4c6 is described below

commit b95b7cd4c6b5acea75b96c852cfa63e3f002c53a
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Sat Sep 26 10:50:58 2026 +0200

    [SONAR] cleanup security backlog
---
 .github/workflows/marketplace_classpath.yml        |  2 +-
 .github/workflows/pr_build_docs.yml                |  2 +-
 .../java/org/apache/hop/core/util/StringUtil.java  |  4 +-
 .../hop/history/local/LocalAuditManager.java       | 65 +---------------------
 .../java/org/apache/hop/www/JdbcTokenServlet.java  |  2 +
 .../main/java/org/apache/hop/lint/LintCommand.java |  2 +
 .../org/apache/hop/vfs/ftp/FtpClientFactory.java   |  2 +
 .../org/apache/hop/ui/hopgui/HopWebAuditPaths.java | 25 ---------
 .../apache/hop/ui/hopgui/HopWebUserFilePlugin.java |  2 +
 .../ui/hopgui/explorer/ExplorerFileServlet.java    |  2 +
 .../hop/ui/hopgui/security/HopBasicAuthFilter.java |  4 +-
 .../hop/ui/hopgui/security/HopLoginPage.java       | 18 +++++-
 .../hop/ui/hopgui/security/HopOidcAuthFilter.java  |  2 +
 .../hop/ui/hopgui/security/HopLoginPageTest.java   | 12 ++++
 14 files changed, 51 insertions(+), 93 deletions(-)

diff --git a/.github/workflows/marketplace_classpath.yml 
b/.github/workflows/marketplace_classpath.yml
index 965baceb31..a16b385a58 100644
--- a/.github/workflows/marketplace_classpath.yml
+++ b/.github/workflows/marketplace_classpath.yml
@@ -126,7 +126,7 @@ jobs:
         continue-on-error: true
         run: |
           set -uo pipefail
-          suites=$(curl -fsSL --max-time 60 \
+          suites=$(curl -fsSL --proto '=https' --proto-redir '=https' 
--max-time 60 \
             "$IT_REPORT_ROOT/api/json?tree=suites\[name\]" | tr ',' '\n' |
             sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | sort -u)
           if [ -z "$suites" ]; then
diff --git a/.github/workflows/pr_build_docs.yml 
b/.github/workflows/pr_build_docs.yml
index 14a71fdfe4..b3c507f06d 100644
--- a/.github/workflows/pr_build_docs.yml
+++ b/.github/workflows/pr_build_docs.yml
@@ -86,7 +86,7 @@ jobs:
 
       - name: Install the site generator
         working-directory: hop-website
-        run: npm ci
+        run: npm ci --ignore-scripts
 
       # Same pre-build steps as the website's `npm run build:hop`: the shared
       # stylesheets, the navigation partials and the browser halves of the
diff --git a/core/src/main/java/org/apache/hop/core/util/StringUtil.java 
b/core/src/main/java/org/apache/hop/core/util/StringUtil.java
index c7d1c584a5..bcc3e8d84e 100644
--- a/core/src/main/java/org/apache/hop/core/util/StringUtil.java
+++ b/core/src/main/java/org/apache/hop/core/util/StringUtil.java
@@ -33,6 +33,8 @@ import org.apache.hop.core.row.IRowMeta;
 /** A collection of utilities to manipulate strings. */
 public class StringUtil {
 
+  // Only used to generate test data, never for anything security related
+  @SuppressWarnings("java:S2245")
   private static final Random random = new Random();
 
   public static final String UNIX_OPEN = "${";
@@ -405,7 +407,7 @@ public class StringUtil {
     }
 
     for (int i = 0; i < length; i++) {
-      int c = 'a' + random.nextInt() * 26;
+      int c = 'a' + random.nextInt(26);
       buffer.append((char) c);
     }
     if (!Utils.isEmpty(postfix)) {
diff --git 
a/engine/src/main/java/org/apache/hop/history/local/LocalAuditManager.java 
b/engine/src/main/java/org/apache/hop/history/local/LocalAuditManager.java
index 979dab9f15..f9f8c898e8 100644
--- a/engine/src/main/java/org/apache/hop/history/local/LocalAuditManager.java
+++ b/engine/src/main/java/org/apache/hop/history/local/LocalAuditManager.java
@@ -20,10 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.nio.file.attribute.PosixFilePermission;
-import java.nio.file.attribute.PosixFilePermissions;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -31,7 +28,6 @@ import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.core.Const;
@@ -83,64 +79,9 @@ public class LocalAuditManager implements IAuditManager {
     }
   }
 
-  /**
-   * Create {@code dir} (and parents) if missing, and best-effort open POSIX 
permissions so Docker
-   * bind mounts remain usable when host UID and container hop UID differ 
(Tomcat often uses umask
-   * 0027 which would otherwise leave 0700/0750 dirs unwritable after a 
host-side chown).
-   */
-  private void ensureWritableDirectory(File dir) throws IOException {
-    if (dir == null) {
-      return;
-    }
-    Path path = dir.toPath().toAbsolutePath().normalize();
-    Files.createDirectories(path);
-    openPermissionsAlongPath(path);
-  }
-
-  /** World rwx on directories from {@link #rootFolder} down to {@code path} 
(best effort). */
-  private void openPermissionsAlongPath(Path path) {
-    if (path == null) {
-      return;
-    }
-    Path root;
-    try {
-      root = Paths.get(rootFolder).toAbsolutePath().normalize();
-    } catch (Exception e) {
-      root = null;
-    }
-    Set<PosixFilePermission> dirPerms = 
PosixFilePermissions.fromString("rwxrwxrwx");
-    Path current = path;
-    while (current != null) {
-      try {
-        if (Files.isDirectory(current)) {
-          try {
-            Files.setPosixFilePermissions(current, dirPerms);
-          } catch (UnsupportedOperationException e) {
-            File f = current.toFile();
-            //noinspection ResultOfMethodCallIgnored
-            f.setReadable(true, false);
-            //noinspection ResultOfMethodCallIgnored
-            f.setWritable(true, false);
-            //noinspection ResultOfMethodCallIgnored
-            f.setExecutable(true, false);
-          }
-        }
-      } catch (IOException | SecurityException e) {
-        // Not owner / read-only FS — caller may still fail on write with a 
clear error
-        LogChannel.GENERAL.logDebug(
-            "LocalAuditManager: could not open permissions on '"
-                + current
-                + "': "
-                + e.getMessage());
-      }
-      if (root != null && current.equals(root)) {
-        break;
-      }
-      Path parent = current.getParent();
-      if (parent == null || parent.equals(current)) {
-        break;
-      }
-      current = parent;
+  private static void ensureWritableDirectory(File dir) throws IOException {
+    if (dir != null) {
+      Files.createDirectories(dir.toPath());
     }
   }
 
diff --git a/engine/src/main/java/org/apache/hop/www/JdbcTokenServlet.java 
b/engine/src/main/java/org/apache/hop/www/JdbcTokenServlet.java
index cb9ee33cc7..3ea17d617f 100644
--- a/engine/src/main/java/org/apache/hop/www/JdbcTokenServlet.java
+++ b/engine/src/main/java/org/apache/hop/www/JdbcTokenServlet.java
@@ -68,6 +68,8 @@ public class JdbcTokenServlet extends BaseHttpServlet 
implements IHopServerPlugi
     return "JDBC token";
   }
 
+  // I/O errors writing the response are left to the servlet container
+  @SuppressWarnings("java:S1989")
   @Override
   public void doGet(HttpServletRequest request, HttpServletResponse response)
       throws ServletException, IOException {
diff --git 
a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java 
b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
index 6d81f3c6ad..cb3e789505 100644
--- a/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
+++ b/plugins/misc/lint/src/main/java/org/apache/hop/lint/LintCommand.java
@@ -195,6 +195,8 @@ public class LintCommand implements Callable<Integer>, 
IHopCommand {
     cmd.setCaseInsensitiveEnumValuesAllowed(true);
   }
 
+  // The stack trace is only printed when the user asks for it with --verbose
+  @SuppressWarnings("java:S4507")
   @Override
   public Integer call() {
     try {
diff --git 
a/plugins/tech/ftp/src/main/java/org/apache/hop/vfs/ftp/FtpClientFactory.java 
b/plugins/tech/ftp/src/main/java/org/apache/hop/vfs/ftp/FtpClientFactory.java
index 3c80805442..3f7d43c03e 100644
--- 
a/plugins/tech/ftp/src/main/java/org/apache/hop/vfs/ftp/FtpClientFactory.java
+++ 
b/plugins/tech/ftp/src/main/java/org/apache/hop/vfs/ftp/FtpClientFactory.java
@@ -244,6 +244,8 @@ public final class FtpClientFactory {
     }
   }
 
+  // Plain FTP is an explicit choice of the connection; FTPS is offered next 
to it
+  @SuppressWarnings("java:S5332")
   private static FTPClient createClient(
       FtpSecurityMode securityMode, IVariables variables, IFtpConnection 
connection)
       throws HopException {
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebAuditPaths.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebAuditPaths.java
index 9f933f433c..ee33379a4d 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebAuditPaths.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebAuditPaths.java
@@ -19,8 +19,6 @@ package org.apache.hop.ui.hopgui;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.PosixFilePermissions;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.logging.LogChannel;
 
@@ -131,7 +129,6 @@ public final class HopWebAuditPaths {
       if (!dir.isDirectory()) {
         return null;
       }
-      openWorldWritable(dir.toPath());
       File probe = new File(dir, ".hop-write-test");
       Files.writeString(probe.toPath(), "ok");
       //noinspection ResultOfMethodCallIgnored
@@ -141,31 +138,9 @@ public final class HopWebAuditPaths {
         //noinspection ResultOfMethodCallIgnored
         users.mkdirs();
       }
-      openWorldWritable(users.toPath());
       return dir;
     } catch (IOException | SecurityException e) {
       return null;
     }
   }
-
-  /**
-   * Best-effort {@code rwxrwxrwx} so host UID and container hop UID (often 
501) can both use a bind
-   * mount. No-op on non-POSIX filesystems or when not the owner.
-   */
-  private static void openWorldWritable(Path path) {
-    if (path == null || !Files.isDirectory(path)) {
-      return;
-    }
-    try {
-      Files.setPosixFilePermissions(path, 
PosixFilePermissions.fromString("rwxrwxrwx"));
-    } catch (UnsupportedOperationException | IOException | SecurityException 
e) {
-      File f = path.toFile();
-      //noinspection ResultOfMethodCallIgnored
-      f.setReadable(true, false);
-      //noinspection ResultOfMethodCallIgnored
-      f.setWritable(true, false);
-      //noinspection ResultOfMethodCallIgnored
-      f.setExecutable(true, false);
-    }
-  }
 }
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebUserFilePlugin.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebUserFilePlugin.java
index 988e075f66..22b54814e7 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebUserFilePlugin.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWebUserFilePlugin.java
@@ -626,6 +626,8 @@ public class HopWebUserFilePlugin {
     }
   }
 
+  // Files.createTempDirectory creates the folder with owner-only (0700) 
permissions on POSIX
+  @SuppressWarnings("java:S5443")
   private static Path getSessionTempDirectory() throws IOException {
     UISession session = RWT.getUISession();
     Path directory = (Path) session.getAttribute(SESSION_TEMP_DIRECTORY);
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/explorer/ExplorerFileServlet.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/explorer/ExplorerFileServlet.java
index 3195e8195f..5e02f8515d 100644
--- 
a/rap/src/main/java/org/apache/hop/ui/hopgui/explorer/ExplorerFileServlet.java
+++ 
b/rap/src/main/java/org/apache/hop/ui/hopgui/explorer/ExplorerFileServlet.java
@@ -47,6 +47,8 @@ public class ExplorerFileServlet extends HttpServlet {
 
   static final long DEFAULT_MAX_BYTES = 16L * 1024L * 1024L;
 
+  // I/O errors writing the response are left to the servlet container
+  @SuppressWarnings("java:S1989")
   @Override
   protected void doGet(HttpServletRequest request, HttpServletResponse 
response)
       throws IOException {
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopBasicAuthFilter.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopBasicAuthFilter.java
index 06743a3184..ad81b6ca91 100644
--- 
a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopBasicAuthFilter.java
+++ 
b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopBasicAuthFilter.java
@@ -142,6 +142,8 @@ public class HopBasicAuthFilter implements Filter {
     chain.doFilter(new HopAuthenticatedRequest(httpRequest, principal), 
response);
   }
 
+  // The username is logged through HopLoginPage.sanitizeForLog
+  @SuppressWarnings("javasecurity:S5145")
   private void handleLoginPost(
       HttpServletRequest request, HttpServletResponse response, String 
contextPath)
       throws IOException {
@@ -157,7 +159,7 @@ public class HopBasicAuthFilter implements Filter {
 
     Optional<HopUser> user = HopUserStore.getInstance().authenticate(username, 
password);
     if (user.isEmpty()) {
-      LOG.log(Level.INFO, "Login failed for user ''{0}''", username);
+      LOG.log(Level.INFO, "Login failed for user ''{0}''", 
HopLoginPage.sanitizeForLog(username));
       showLoginPage(request, response, contextPath, "Invalid username or 
password.", username);
       return;
     }
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopLoginPage.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopLoginPage.java
index 59f7cb4705..adaec4a3b3 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopLoginPage.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopLoginPage.java
@@ -243,8 +243,12 @@ public final class HopLoginPage {
       return fallback;
     }
     String r = redirect.trim();
-    // Block open redirects
-    if (r.startsWith("http://";) || r.startsWith("https://";) || 
r.startsWith("//")) {
+    // Block open redirects. Browsers treat a backslash as a slash, so 
"/\host" means "//host".
+    if (r.startsWith("http://";)
+        || r.startsWith("https://";)
+        || r.startsWith("//")
+        || r.indexOf('\\') >= 0
+        || r.chars().anyMatch(Character::isISOControl)) {
       return fallback;
     }
     if (!r.startsWith("/")) {
@@ -260,6 +264,16 @@ public final class HopLoginPage {
     return r;
   }
 
+  /** Replaces control characters (CR, LF, ...) so user input cannot forge 
extra log lines. */
+  public static String sanitizeForLog(String raw) {
+    if (raw == null) {
+      return null;
+    }
+    StringBuilder sb = new StringBuilder(raw.length());
+    raw.codePoints().forEach(c -> sb.appendCodePoint(Character.isISOControl(c) 
? '_' : c));
+    return sb.toString();
+  }
+
   static String escapeHtml(String raw) {
     if (raw == null) {
       return "";
diff --git 
a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopOidcAuthFilter.java 
b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopOidcAuthFilter.java
index 19cc314e64..b8bfaeb8f0 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopOidcAuthFilter.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/security/HopOidcAuthFilter.java
@@ -301,6 +301,8 @@ public class HopOidcAuthFilter implements Filter {
     response.sendRedirect(contextPath + HopLoginPage.PATH_LOGIN + "?logout=1");
   }
 
+  // HopLoginPage sanitizes the redirect and HTML-escapes every value it 
renders
+  @SuppressWarnings("javasecurity:S5131")
   private void showLoginPage(
       HttpServletRequest request,
       HttpServletResponse response,
diff --git 
a/rap/src/test/java/org/apache/hop/ui/hopgui/security/HopLoginPageTest.java 
b/rap/src/test/java/org/apache/hop/ui/hopgui/security/HopLoginPageTest.java
index 0be92df6e4..2682fda121 100644
--- a/rap/src/test/java/org/apache/hop/ui/hopgui/security/HopLoginPageTest.java
+++ b/rap/src/test/java/org/apache/hop/ui/hopgui/security/HopLoginPageTest.java
@@ -17,7 +17,9 @@
 
 package org.apache.hop.ui.hopgui.security;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
@@ -40,6 +42,16 @@ class HopLoginPageTest {
     assertTrue(HopLoginPage.escapeHtml("<x>").contains("&lt;"));
     assertTrue(HopLoginPage.sanitizeRedirect("https://evil.example/";, 
"").endsWith("/ui"));
     assertTrue(HopLoginPage.sanitizeRedirect("/ui-dark", 
"").equals("/ui-dark"));
+    assertEquals("/ui", HopLoginPage.sanitizeRedirect("/\\evil.example", ""));
+    assertEquals("/ui", HopLoginPage.sanitizeRedirect("\\\\evil.example", ""));
+    assertEquals("/ui", HopLoginPage.sanitizeRedirect("/ui\r\nX-Header: y", 
""));
+  }
+
+  @Test
+  void sanitizeForLogReplacesControlCharacters() {
+    assertEquals("alice__forged", 
HopLoginPage.sanitizeForLog("alice\r\nforged"));
+    assertEquals("bob", HopLoginPage.sanitizeForLog("bob"));
+    assertNull(HopLoginPage.sanitizeForLog(null));
   }
 
   @Test

Reply via email to