/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.utils.common.security;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Unit test for {@link StringSubstitutionSecurityUtils}.
*
* @author Robert Mischke
*/
// TODO (p1) review pattern lists for completeness
public final class StringSubstitutionSecurityUtils {
private static final String[] COMMON_FORBIDDEN_PATTERNS = {
"\"", // breaking out of surrounding double quotes
"[\\u0000-\\u001f]", // ASCII characters 0-31 (including \0, newlines and tabs)
"\\\\", // backslash - character escaping, path traversal (escaped for java and regexp -> 4x)
"/", // forward slash - path traversal
"\\*", "\\?", // file system wildcards
};
// option for unit tests; see setter method
private static boolean suppressLogMessageOnDeniedSubstitution = false;
private static final Log LOG = LogFactory.getLog(StringSubstitutionSecurityUtils.class);
/**
* Provides identifiers for various contexts that strings may be substituted/inserted into.
*
* @author Robert Mischke
*/
public enum SubstitutionContext {
/** Context identifier for Windows batch files. */
WINDOWS_BATCH(new String[] {
"%", // variable substitution - access to environment variables ("%env%")
}),
/** Context identifier for Linux bash scripts. */
LINUX_BASH(new String[] {
"\u0060", // backtick - command substitution
"\\$", // bash substitution - command substitution ("$(...)"), access to environment variables ("$VAR")
}),
/** Context identifier for Jython script code. */
JYTHON(new String[] {
"'" // forbid single quote too until proven safe
});
private static final String REGEXP_ALTERNATIVES_JOINER = "|";
private final Pattern forbiddenCharactersRegexp;
SubstitutionContext(String[] customPatterns) {
String patternString = StringUtils.join(COMMON_FORBIDDEN_PATTERNS, REGEXP_ALTERNATIVES_JOINER);
// don't break regexp if there are no custom patterns for a context
if (customPatterns.length > 0) {
patternString += REGEXP_ALTERNATIVES_JOINER + StringUtils.join(customPatterns, REGEXP_ALTERNATIVES_JOINER);
}
this.forbiddenCharactersRegexp = Pattern.compile(patternString);
}
public Pattern getForbiddenCharactersRegexp() {
return forbiddenCharactersRegexp;
}
}
private StringSubstitutionSecurityUtils() {}
/**
* Tests whether the given string can be safely inserted/substituted into the given target context if it is surrounded by double quotes
* (<code>other text/code "<tested string>" other text/code</code>).
*
* @param string the string content to test
* @param context the context the string should be tested against (and if it is considered safe, inserted into)
* @return true if the given string is considered safe; false if it is not
*/
public static boolean isSafeForSubstitutionInsideDoubleQuotes(String string, SubstitutionContext context) {
if (string == null) {
throw new NullPointerException("The substitution string can not be 'null'");
}
if (context == null) {
throw new NullPointerException("Internal error: Subsctitution context is 'null'");
}
Matcher matcher = context.getForbiddenCharactersRegexp().matcher(string);
if (matcher.find()) {
if (!suppressLogMessageOnDeniedSubstitution) {
LOG.warn(de.rcenvironment.core.utils.common.StringUtils.format(
"Denied string \"%s\" for substitution in context %s because of insecure character sequence <%s>", string,
context.name(), matcher.group(0)));
}
return false;
} else {
return true;
}
}
/**
* Custom option to make unit tests less verbose.
*
* @param suppress true to suppress the "Denied string ... for substitution" if an insecure string is rejected
*/
protected static void setSuppressLogMessageOnDeniedSubstitution(boolean suppress) {
suppressLogMessageOnDeniedSubstitution = suppress;
}
}