diff --git a/framework.tck/pom.xml b/framework.tck/pom.xml
index 35cd65a10d..f5ca8d41fd 100644
--- a/framework.tck/pom.xml
+++ b/framework.tck/pom.xml
@@ -105,7 +105,7 @@
net.bytebuddy
byte-buddy
- 1.17.5
+ 1.18.0
test
@@ -132,12 +132,6 @@
1.12.1
test
-
- org.junit.platform
- junit-platform-launcher
- 1.12.1
- test
-
org.osgi
org.osgi.test.cases.framework
diff --git a/framework.tck/tck.bndrun b/framework.tck/tck.bndrun
index 541ec5ddb6..4adf75de7c 100644
--- a/framework.tck/tck.bndrun
+++ b/framework.tck/tck.bndrun
@@ -30,7 +30,7 @@
junit-platform-engine;version='[1.12.1,1.12.2)',\
org.opentest4j;version='[1.3.0,1.3.1)',\
junit-platform-launcher;version='[1.12.1,1.12.2)',\
- assertj-core;version='[3.27.3,3.27.4)',\
biz.aQute.junit;version='[6.4.1,6.4.2)',\
junit-vintage-engine;version='[5.7.1,5.7.2)',\
- net.bytebuddy.byte-buddy;version='[1.17.5,1.17.6)'
\ No newline at end of file
+ assertj-core;version='[3.27.7,3.27.8)',\
+ net.bytebuddy.byte-buddy;version='[1.18.0,1.18.1)'
\ No newline at end of file
diff --git a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
index 2708f24c9e..e19f13d9cf 100644
--- a/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
+++ b/gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
@@ -1635,7 +1635,14 @@ protected void grep(CommandSession session, Process process, String[] argv) thro
if (line.length() == 1 && line.charAt(0) == '\n') {
break;
}
- boolean matches = p.matcher(line).matches();
+ boolean matches;
+ try {
+ matches = p.matcher(new TimeoutCharSequence(line, REGEX_TIMEOUT_MS)).matches();
+ } catch (RegexTimeoutException e) {
+ process.err().println("grep: pattern matching timed out (possible ReDoS attack)");
+ process.error(1);
+ return;
+ }
AttributedStringBuilder sbl = new AttributedStringBuilder();
if (!count) {
if (sources.size() > 1) {
@@ -1664,21 +1671,27 @@ protected void grep(CommandSession session, Process process, String[] argv) thro
applyStyle(sbl, colors, style);
}
AttributedString aLine = AttributedString.fromAnsi(line);
- Matcher matcher2 = p2.matcher(aLine.toString());
+ Matcher matcher2 = p2.matcher(new TimeoutCharSequence(aLine.toString(), REGEX_TIMEOUT_MS));
int cur = 0;
- while (matcher2.find()) {
- int index = matcher2.start(0);
- AttributedString prefix = aLine.subSequence(cur, index);
- sbl.append(prefix);
- cur = matcher2.end();
- if (colored) {
- applyStyle(sbl, colors, invertMatch ? "mc" : "ms", "mt");
- }
- sbl.append(aLine.subSequence(index, cur));
- if (colored) {
- applyStyle(sbl, colors, style);
+ try {
+ while (matcher2.find()) {
+ int index = matcher2.start(0);
+ AttributedString prefix = aLine.subSequence(cur, index);
+ sbl.append(prefix);
+ cur = matcher2.end();
+ if (colored) {
+ applyStyle(sbl, colors, invertMatch ? "mc" : "ms", "mt");
+ }
+ sbl.append(aLine.subSequence(index, cur));
+ if (colored) {
+ applyStyle(sbl, colors, style);
+ }
+ nb++;
}
- nb++;
+ } catch (RegexTimeoutException e) {
+ process.err().println("grep: pattern matching timed out (possible ReDoS attack)");
+ process.error(1);
+ return;
}
sbl.append(aLine.subSequence(cur, aLine.length()));
}
@@ -2148,4 +2161,63 @@ public Long lines() {
return null;
}
}
+
+ private static final long REGEX_TIMEOUT_MS = 1000;
+
+ private static class RegexTimeoutException extends RuntimeException {
+ public RegexTimeoutException(String message) {
+ super(message);
+ }
+ }
+
+ private static class TimeoutCharSequence implements CharSequence {
+ private final CharSequence seq;
+ private final long startTime;
+ private final long timeoutNanos;
+ private int count = 0;
+ private static final int CHECK_INTERVAL = 1000;
+
+ public TimeoutCharSequence(CharSequence seq, long timeoutMs) {
+ this.seq = seq;
+ this.startTime = System.nanoTime();
+ this.timeoutNanos = (timeoutMs >= Long.MAX_VALUE / 1000000) ? Long.MAX_VALUE : timeoutMs * 1000000;
+ }
+
+ private TimeoutCharSequence(CharSequence seq, long startTime, long timeoutNanos) {
+ this.seq = seq;
+ this.startTime = startTime;
+ this.timeoutNanos = timeoutNanos;
+ }
+
+ @Override
+ public int length() {
+ checkTimeout();
+ return seq.length();
+ }
+
+ @Override
+ public char charAt(int index) {
+ checkTimeout();
+ return seq.charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return new TimeoutCharSequence(seq.subSequence(start, end), startTime, timeoutNanos);
+ }
+
+ @Override
+ public String toString() {
+ return seq.toString();
+ }
+
+ private void checkTimeout() {
+ if (++count >= CHECK_INTERVAL) {
+ count = 0;
+ if (System.nanoTime() - startTime > timeoutNanos) {
+ throw new RegexTimeoutException("Regular expression matching timed out");
+ }
+ }
+ }
+ }
}
diff --git a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java
index 7317283a58..c9e1e463ab 100644
--- a/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java
+++ b/gogo/jline/src/test/java/org/apache/felix/gogo/jline/PosixTest.java
@@ -101,6 +101,57 @@ public void testWcLinesBytesChar() throws Exception {
assertEquals(" 1 5 5", res);
}
+ @Test
+ public void testGrepReDosTimeout() throws Exception {
+ Context context = new Context();
+ context.addCommand("echo", new Posix(context));
+ context.addCommand("grep", new Posix(context));
+ context.addCommand("tac", this);
+
+ // This pattern (a+)+ with input aaaa...b causes catastrophic backtracking.
+ // It will trigger our ReDoS protection timeout (1 second).
+ long start = System.currentTimeMillis();
+ Object res = context.execute("echo \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab\" | grep -x \"(a+)+\" | tac");
+ long duration = System.currentTimeMillis() - start;
+
+ // Assert that matching was aborted and didn't hang indefinitely (timeout is 1 second)
+ assertTrue("Matching should time out within 4 seconds", duration < 4000);
+ assertEquals("", res);
+ }
+
+ @Test
+ public void testTimeoutCharSequenceLargeTimeout() throws Exception {
+ Class> clazz = Class.forName("org.apache.felix.gogo.jline.Posix$TimeoutCharSequence");
+ java.lang.reflect.Constructor> constructor = clazz.getDeclaredConstructor(CharSequence.class, long.class);
+ constructor.setAccessible(true);
+
+ CharSequence seq = (CharSequence) constructor.newInstance("hello", Long.MAX_VALUE);
+ assertEquals(5, seq.length());
+ assertEquals('h', seq.charAt(0));
+
+ CharSequence sub = seq.subSequence(1, 4);
+ assertEquals(3, sub.length());
+ assertEquals('e', sub.charAt(0));
+ }
+
+ @Test
+ public void testTimeoutCharSequenceTriggersTimeout() throws Exception {
+ Class> clazz = Class.forName("org.apache.felix.gogo.jline.Posix$TimeoutCharSequence");
+ java.lang.reflect.Constructor> constructor = clazz.getDeclaredConstructor(CharSequence.class, long.class);
+ constructor.setAccessible(true);
+
+ CharSequence seq = (CharSequence) constructor.newInstance("hello", 0L);
+ try {
+ for (int i = 0; i < 2000; i++) {
+ seq.length();
+ }
+ fail("Expected RegexTimeoutException to be thrown");
+ } catch (Exception e) {
+ assertEquals("org.apache.felix.gogo.jline.Posix$RegexTimeoutException", e.getClass().getName());
+ assertEquals("Regular expression matching timed out", e.getMessage());
+ }
+ }
+
public String tac() throws IOException {
StringWriter sw = new StringWriter();
Reader rdr = new InputStreamReader(System.in);
diff --git a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java
index eee356351c..1345cdae27 100644
--- a/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java
+++ b/gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java
@@ -148,14 +148,24 @@ public boolean grep(CommandSession session, String[] argv) throws IOException
while ((s = rdr.readLine()) != null)
{
line++;
- Matcher matcher = pattern.matcher(s);
- if (matcher.find() == !opt.isSet("invert-match"))
+ try
{
- match = true;
- if (opt.isSet("quiet"))
- break;
+ TimeoutCharSequence timeoutSeq = new TimeoutCharSequence(s, REGEX_TIMEOUT_MS);
+ Matcher matcher = pattern.matcher(timeoutSeq);
+ if (matcher.find() == !opt.isSet("invert-match"))
+ {
+ match = true;
+ if (opt.isSet("quiet"))
+ break;
- System.out.println(String.format(format, arg, line, s));
+ System.out.println(String.format(format, arg, line, s));
+ }
+ }
+ catch (RegexTimeoutException e)
+ {
+ System.err.println("grep: pattern matching timed out (possible ReDoS attack)");
+ status = false;
+ break;
}
}
@@ -200,4 +210,74 @@ public static void copy(InputStream in, OutputStream out) throws IOException
out.flush();
}
+ private static final long REGEX_TIMEOUT_MS = 1000;
+
+ private static class RegexTimeoutException extends RuntimeException
+ {
+ public RegexTimeoutException(String message)
+ {
+ super(message);
+ }
+ }
+
+ private static class TimeoutCharSequence implements CharSequence
+ {
+ private final CharSequence seq;
+ private final long startTime;
+ private final long timeoutNanos;
+ private int count = 0;
+ private static final int CHECK_INTERVAL = 1000;
+
+ public TimeoutCharSequence(CharSequence seq, long timeoutMs)
+ {
+ this.seq = seq;
+ this.startTime = System.nanoTime();
+ this.timeoutNanos = (timeoutMs >= Long.MAX_VALUE / 1000000) ? Long.MAX_VALUE : timeoutMs * 1000000;
+ }
+
+ private TimeoutCharSequence(CharSequence seq, long startTime, long timeoutNanos)
+ {
+ this.seq = seq;
+ this.startTime = startTime;
+ this.timeoutNanos = timeoutNanos;
+ }
+
+ @Override
+ public int length()
+ {
+ checkTimeout();
+ return seq.length();
+ }
+
+ @Override
+ public char charAt(int index)
+ {
+ checkTimeout();
+ return seq.charAt(index);
+ }
+
+ @Override
+ public CharSequence subSequence(int start, int end)
+ {
+ return new TimeoutCharSequence(seq.subSequence(start, end), startTime, timeoutNanos);
+ }
+
+ @Override
+ public String toString()
+ {
+ return seq.toString();
+ }
+
+ private void checkTimeout()
+ {
+ if (++count >= CHECK_INTERVAL)
+ {
+ count = 0;
+ if (System.nanoTime() - startTime > timeoutNanos)
+ {
+ throw new RegexTimeoutException("Regular expression matching timed out");
+ }
+ }
+ }
+ }
}