Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions framework.tck/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.17.5</version>
<version>1.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -132,12 +132,6 @@
<version>1.12.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.12.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.test.cases.framework</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions framework.tck/tck.bndrun
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
assertj-core;version='[3.27.7,3.27.8)',\
net.bytebuddy.byte-buddy;version='[1.18.0,1.18.1)'
100 changes: 86 additions & 14 deletions gogo/jline/src/main/java/org/apache/felix/gogo/jline/Posix.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()));
}
Expand Down Expand Up @@ -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");
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
92 changes: 86 additions & 6 deletions gogo/shell/src/main/java/org/apache/felix/gogo/shell/Posix.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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");
}
}
}
}
}