This is an automated email from the ASF dual-hosted git repository.
pdallig pushed a commit to branch branch-0.12
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/branch-0.12 by this push:
new d3bbd11295 [ZEPPELIN-6190] Prevent directory escape bypass through
repeated URL decoding
d3bbd11295 is described below
commit d3bbd11295e75590fbcfcbf256c7acb78ee5f266
Author: ChanHo Lee <[email protected]>
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 <[email protected]>
---
.../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");