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"); + } + } + } + } }