package org.ovirt.engine.core.searchbackend;
import java.util.HashSet;
import org.ovirt.engine.core.common.errors.SearchEngineIllegalCharacterException;
import org.ovirt.engine.core.compat.StringFormat;
import org.ovirt.engine.core.compat.StringTokenizerCompat;
public abstract class SqlInjectionChecker {
private static final char QUOTE = '\'';
private static final char BACKSLASH = '\\';
private static final char PERCENT = '%';
private static final char BLANK = ' ';
private static final String BACKSLASH_STR = "\\";
private static final String QUOTE_STR = "'";
private static final String DOUBLE_QUOTE_STR = "\"";
private static final String BACKSLASH_QUOTE = "\\\\'";
private static final String BACKSLASH_DOUBLE_QUOTE = "\\\\\"";
private static final String QUOTE_QUOTE = "''";
private static final String DELIMITERS = "'\"";
private HashSet<String> sqlInjectionExpressions = new HashSet<String>();
private static final String[] ANSI_SQL_KEYWORDS = { " insert ", " delete ", " update ", " create ", " drop ", " union ", " alter ",
" if ", " else ", "sum(", "min(", "max(", "count(", "avg(", " having "};
SqlInjectionChecker() {
for (String s : ANSI_SQL_KEYWORDS) {
addInjectionExpression(s);
}
sqlInjectionExpressions.add(getSqlCommandSeperator());
sqlInjectionExpressions.add(getSqlConcat());
sqlInjectionExpressions.addAll(getCommentExpressions());
sqlInjectionExpressions.addAll(getInjectionFunctions());
}
/**
* Adds an entry to injection expressions.
* @param Expr the expression.
*/
public void addInjectionExpression(String Expr){
sqlInjectionExpressions.add(Expr);
}
/**
* Checks if the given sql has SQL Injection expressions
* @param sql the sql string
* @return boolean
*/
public boolean hasSqlInjection(String sql) {
sql = removeAllStringValuesFromSql(sql);
if (sql.length() > 0) {
// replace all functions to have the format "f(" in order to match it exactly.
sql = sql.replaceAll("\\s+\\(", "(");
// look for sql injection expressions
for (String expr : sqlInjectionExpressions) {
if (sql.contains(expr)) {
return true;
}
}
}
return false;
}
/**
* Removes all values enclosed by single or double quotes from original sql
* in order to test for injection only on sql keywords and not on values.
* @param sql
* @return
*/
private String removeAllStringValuesFromSql(String sql) {
boolean singleQuoteFound=false;
boolean doubleQuoteFound=false;
StringBuffer sb = new StringBuffer();
// replace all occurrences of a quote/s inside a value with an empty string.
final String[] QUOTES_INSIDE_VALUE_INDICATORES = {BACKSLASH_QUOTE,QUOTE_QUOTE,BACKSLASH_DOUBLE_QUOTE};
for (String s : QUOTES_INSIDE_VALUE_INDICATORES) {
sql = sql.replaceAll(s, "");
}
StringTokenizerCompat st = new StringTokenizerCompat(sql, DELIMITERS, true);
while(st.hasMoreTokens()) {
String token = st.nextToken();
if (token.equals(QUOTE_STR)) {
if(singleQuoteFound){
singleQuoteFound = false; // closing '
continue;
}
else if (!doubleQuoteFound){ // ignore single quote inside double quotes
singleQuoteFound = true; // opening '
continue;
}
}
else if (token.equals(DOUBLE_QUOTE_STR)) {
if(doubleQuoteFound){
doubleQuoteFound = false; // closing ""
continue;
}
else if (!singleQuoteFound){ // ignore double quote inside single quotes
doubleQuoteFound = true; // opening "
continue;
}
}
if (!singleQuoteFound && !doubleQuoteFound) {
sb.append(token);
}
}
return sb.toString();
}
/**
* Enforce escaping special characters in an expression by proceeding them with a backslash.
* @param value the expression value
* @return String the formatted expression.
*/
public static String enforceEscapeCharacters(String value) {
StringBuilder sb = new StringBuilder();
if (value.indexOf(QUOTE) >= 0 || value.indexOf(BACKSLASH) >= 0 || value.indexOf(PERCENT) >= 0) {
// the following is a Postgres limitation, since we are using LIKE/ILIKE and
// Postgres does not allow that last value character is the default ESCAPE ('\')
if (value.endsWith(BACKSLASH_STR)) {
throw new SearchEngineIllegalCharacterException();
}
char[] sourceArray = value.toCharArray();
int i = 0;
char prev = BLANK;
char next = BLANK;
for (Character c : sourceArray) {
switch (c) {
case QUOTE:
case PERCENT: // Those values should be formatted as \' or \%
if (i > 0 && prev == BACKSLASH) {
sb.append(c);
} else {
sb.append(BACKSLASH);
sb.append(c);
}
break;
case BACKSLASH: // A backslash should be formatted as \\
if ((i > 0 && prev == BACKSLASH) || (next == QUOTE || next == PERCENT || next == BACKSLASH)) {
sb.append(c);
} else {
sb.append(BACKSLASH);
sb.append(BACKSLASH);
}
break;
default: // regular , copy character as is
sb.append(c);
break;
}
prev = c.charValue();
i++;
next = (i < sourceArray.length - 1) ? sourceArray[i + 1] : BLANK;
}
} else {
sb.append(value);
}
return StringFormat.format("'%1$s'",sb.toString());
}
/**
* gets the database vendor specific sql command separator
* @return
*/
abstract protected String getSqlCommandSeperator();
/**
* gets the database vendor specific sql string concatenation
* @return
*/
abstract protected String getSqlConcat();
/**
* gets the database vendor specific sql comment begin/end definition
* @return
*/
abstract protected HashSet<String> getCommentExpressions();
/**
* gets the database vendor specific functions that are considered as sql injection.
* @return
*/
abstract protected HashSet<String> getInjectionFunctions();
}