...
Suppose a system log file contains messages output by various system processes. Some processes produce public messages and some processes produce sensitive messages marked "private." Here is an example log file:
Code Block |
---|
10:47:03 private[423] Successful logout name: usr1 ssn: 111223333
10:47:04 public[48964] Failed to resolve network service
10:47:04 public[1] (public.message[49367]) Exited with exit code: 255
10:47:43 private[423] Successful login name: usr2 ssn: 444556666
10:48:08 public[48964] Backup failed with error: 19
|
A user wishes to search the log file for interesting messages but must be prevented from seeing the private messages. A program might accomplish this by permitting the user to provide search text that becomes part of the following regex:
Code Block |
---|
(.*? +public\[\d+\] +.*<SEARCHTEXT>.*)
|
However, if an attacker can substitute any string for <SEARCHTEXT>, he can perform a regex injection with the following text:
Code Block |
---|
.*)|(.*
|
When injected into the regex, the regex becomes:
Code Block |
---|
(.*? +public\[\d+\] +.*.*)|(.*.*)
|
This regex will match any line in the log file, including the private ones.
Noncompliant Code Example
This noncompliant code example searches a log file using search terms from an untrusted user.
Code Block | ||
---|---|---|
| ||
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class LogSearch {
public static void FindLogEntry(String search) {
// Construct regex dynamically from user string
String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
Pattern keywordPattern = Pattern.compile(regex);
try (FileInputStream fis = new FileInputStream("log.txt")) {
FileChannel channel = fis.getChannel();
// Get the file's size and map it into memory
long size = channel.size();
final MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, size);
Charset charset = Charset.forName("ISO-8859-15");
final CharsetDecoder decoder = charset.newDecoder();
// Read file into char buffer
CharBuffer log = decoder.decode(mappedBuffer);
Matcher logMatcher = keywordPattern.matcher(log);
while (logMatcher.find()) {
String match = logMatcher.group(1);
if (match != null) {
System.out.println(match);
}
}
} catch (IOException ex) {
System.err.println("thrown exception: " + ex.toString());
Throwable[] suppressed = ex.getSuppressed();
for (int i = 0; i < suppressed.length; i++) {
System.err.println("suppressed exception: "
+ suppressed[i].toString());
}
}
return;
}
public static void main(String[] args) {
FindLogEntry(args[0]);
}
}
|
This code permits an attacker to perform a regex injection.
Noncompliant Code Example
This noncompliant code example periodically loads the log file into memory and allows clients to obtain keyword search suggestions by passing the keyword as an argument to suggestSearches()
.
Code Block | ||
---|---|---|
| ||
public class Keywords {
private static ScheduledExecutorService scheduler
= Executors.newSingleThreadScheduledExecutor();
private static CharBuffer log;
private static final Object lock = new Object();
// Map log file into memory, and periodically reload
static {
try {
FileChannel channel = new FileInputStream(
"path").getChannel();
// Get the file's size and map it into memory
int size = (int) channel.size();
final MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, size);
Charset charset = Charset.forName("ISO-8859-15");
final CharsetDecoder decoder = charset.newDecoder();
log = decoder.decode(mappedBuffer); // Read file into char buffer
Runnable periodicLogRead = new Runnable() {
@Override public void run() {
synchronized(lock) {
try {
log = decoder.decode(mappedBuffer);
} catch (CharacterCodingException e) {
// Forward to handler
}
}
}
};
scheduler.scheduleAtFixedRate(periodicLogRead,
0, 5, TimeUnit.SECONDS);
} catch (Throwable t) {
// Forward to handler
}
}
public static Set<String> suggestSearches(String search) {
synchronized(lock) {
Set<String> searches = new HashSet<String>();
// Construct regex dynamically from user string
String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
Pattern keywordPattern = Pattern.compile(regex);
Matcher logMatcher = keywordPattern.matcher(log);
while (logMatcher.find()) {
String found = logMatcher.group(1);
searches.add(found);
}
return searches;
}
}
}
|
...
This compliant solution filters out nonalphanumeric characters (except space and single quote) from the search string, which prevents regex injection.
Code Block | ||
---|---|---|
| ||
public class Keywords {
// ...
public static Set<String> suggestSearches(String search) {
synchronized (lock) {
Set<String> searches = new HashSet<String>();
StringBuilder sb = new StringBuilder(search.length());
for (int i = 0; i < search.length(); ++i) {
char ch = search.charAt(i);
if (Character.isLetterOrDigit(ch) ||
ch == ' ' ||
ch == '\'') {
sb.append(ch);
}
}
search = sb.toString();
// Construct regex dynamically from user string
String regex = "(.*? +public\\[\\d+\\] +.*" + search + ".*)";
// ...
}
}
}
|
...