This is an automated email from the ASF dual-hosted git repository. pdallig pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push: new 588b54bf54 [ZEPPELIN-6190] Prevent directory escape bypass through repeated URL decoding 588b54bf54 is described below commit 588b54bf54cbbf845af5ca0c9369ae4a74ec3ddf Author: ChanHo Lee <chanho0...@gmail.com> AuthorDate: Tue Jun 3 22:13:59 2025 +0900 [ZEPPELIN-6190] Prevent directory escape bypass through repeated URL decoding ### What is this PR for? This PR addresses an issue in `NotebookService` where the notebook path validation only performs a single decoding pass. This allowed a malicious user to bypass validation by double-encoding the `".."` token. By implementing the repeated decoding, we can prevent this bypass. Additionally, to prevent excessive decoding attempts, a maximum limit on the number of decoding attempts has been added. ### What type of PR is it? Hot Fix ### What is the Jira issue? https://issues.apache.org/jira/projects/ZEPPELIN/issues/ZEPPELIN-6190 ### How should this be tested? * CI ### Questions: * Does the license files need to update? No * Is there breaking changes for older versions? * There may be minor compatibility issues if a user relies on multiple encoded paths, but this is unlikely in realistic scenarios. * Does this needs documentation? No Closes #4891 from tbonelee/fix-validating-note-path. Signed-off-by: Philipp Dallig <philipp.dal...@gmail.com> --- .../org/apache/zeppelin/service/NotebookService.java | 20 +++++++++++++++++++- .../apache/zeppelin/service/NotebookServiceTest.java | 13 +++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java index c924ed898b..3947f4f186 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/service/NotebookService.java @@ -24,6 +24,7 @@ import static org.apache.zeppelin.interpreter.InterpreterResult.Code.ERROR; import static org.apache.zeppelin.scheduler.Job.Status.ABORT; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.text.ParseException; @@ -239,7 +240,7 @@ public class NotebookService { notePath = notePath.replace("\r", " ").replace("\n", " "); - notePath = URLDecoder.decode(notePath, StandardCharsets.UTF_8.toString()); + notePath = decodeRepeatedly(notePath); if (notePath.endsWith("/")) { throw new IOException("Note name shouldn't end with '/'"); } @@ -1563,4 +1564,21 @@ public class NotebookService { return false; } } + + private static String decodeRepeatedly(final String encoded) throws IOException { + String previous = encoded; + int maxDecodeAttempts = 5; + int attempts = 0; + + while (attempts < maxDecodeAttempts) { + String decoded = URLDecoder.decode(previous, StandardCharsets.UTF_8); + attempts++; + if (decoded.equals(previous)) { + return decoded; + } + previous = decoded; + } + + throw new IOException("Exceeded maximum decode attempts. Possible malicious input."); + } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java index be30a3cda8..152d085668 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/service/NotebookServiceTest.java @@ -615,6 +615,19 @@ class NotebookServiceTest { } catch (IOException e) { assertEquals("Note name can not contain '..'", e.getMessage()); } + try { + // Double URL encoding of ".." + notebookService.normalizeNotePath("%252e%252e/%252e%252e/tmp/test333"); + fail("Should fail"); + } catch (IOException e) { + assertEquals("Note name can not contain '..'", e.getMessage()); + } + try { + notebookService.normalizeNotePath("%252525252e%252525252e/tmp/test444"); + fail("Should fail"); + } catch (IOException e) { + assertEquals("Exceeded maximum decode attempts. Possible malicious input.", e.getMessage()); + } try { notebookService.normalizeNotePath("./"); fail("Should fail");