/*
* Copyright 2009-2012 Michael Dalton
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jtaint;
import java.sql.Connection;
/* Oracle DB - supports versions 9i - 11g
* Lexical changes between 9i and 11g
*
* Feature: support for quote delimited strings
* Added: 10g release 1
*
* Feature: Binary floating point numeric literals
* Added: 10g release 1
*
* Feature: Escape character used to escape single quote in string literal
* NOT ADDED: Mentioned in 10g / 11g, but manual analysis confirms that
* escape characters _cannot_ be used to escape single quotes in
* string literals. Oracle Documentation is *INCORRECT* here.
*
*/
public final class OracleSqlValidator extends SqlValidator
{
private final boolean binaryFloatingPointNumbers;
private final boolean quoteDelimitedStrings;
public boolean isSqlOperator(char c) {
return "#[]{}(),.;:+-*/%&<>=|_^?".indexOf(c) != -1;
}
public int parseOracleQuoteDelimitedString(String s, int offset) {
int len = s.length();
Taint t = s.@internal@taint();
if (offset == len - 1) {
SqlUtil.parseError(s, "Unterminated quote delimited string");
return offset;
}
char quoteDelim = s.charAt(++offset);
int origQuoteDelimOffset = offset;
if (t.get(offset))
SqlUtil.abortQuery(s,"Tainted delimiter in quote delimited string");
if (quoteDelim == ' ' || quoteDelim == '\t' || quoteDelim == '\n') {
SqlUtil.parseError(s, "Forbidden whitespace quote delimiter");
return offset;
}
switch(quoteDelim) {
case '[':
quoteDelim = ']';
break;
case '{':
quoteDelim = '}';
break;
case '<':
quoteDelim = '>';
break;
case '(':
quoteDelim = ')';
break;
}
while (++offset < len) {
char c = s.charAt(offset);
if (c == '\0') {
if (t.get(offset))
SqlUtil.abortQuery(s, "Tainted null byte in string");
Log.warn(new Throwable("Null byte in SQL Query: " + s));
}
if (c != '\'' || s.charAt(offset - 1) != quoteDelim
|| offset == origQuoteDelimOffset + 1)
continue;
if (t.get(offset) || t.get(offset - 1))
SqlUtil.abortQuery(s, "Tainted character terminates quote "
+ "delimited string");
return offset;
}
SqlUtil.parseError(s, "Unterminated quote delimited string literal");
return offset;
}
public int parseOracleString(String s, int offset)
{
Taint t = s.@internal@taint();
/* Check for quote delimited string */
if (quoteDelimitedStrings && offset >= 1
&& (s.charAt(offset-1) == 'q' || s.charAt(offset-1) == 'Q')) {
if (t.get(offset-1))
SqlUtil.abortQuery(s, "Tainted quote delimited string");
else if (offset == 1 || isSqlWhitespace(s.charAt(offset - 2))
|| isSqlOperator(s.charAt(offset - 2))
|| isSqlSpecial(s.charAt(offset - 2)))
return parseOracleQuoteDelimitedString(s, offset);
}
return SqlParseUtil.parseStringLiteral(s, offset, false);
}
public int parseOracleIdentifier(String s, int offset)
{
int len = s.length();
Taint t = s.@internal@taint();
while (++offset < len) {
if (t.get(offset))
SqlUtil.abortQuery(s, "Tainted identifier");
else if (s.charAt(offset) == '"')
return offset;
}
SqlUtil.parseError(s, "Unterminated quoted identifier");
return offset;
}
/** Parse a tainted value -- which must be a numeric literal
* for safety reasons. Any other token (identifier, string literal,
* keyword, operator) is considered unsafe and will result in a security
* exception. The tainted numeric literal must occur at the beginning of
* the query, or be preceded by whitespace or a valid SQL operator.
* True and false boolean constants are considered numeric literals.
* @param s query String
* @param offset current position of the lexer
* @return last character (inclusive) of tainted number or -1.
*
* Oracle differs from other databases in that a trailing type specifier
* is allowed for numeric literals. Valid specifiers are f or F for binary
* floating point, and d or D for double precision binary floating point.
*/
public int parseOracleTaintedValue(String s, int offset,
boolean binaryFloatingPointNumbers)
{
int len = s.length();
if (!binaryFloatingPointNumbers)
return SqlParseUtil.parseTaintedValue(s, offset, this);
/* Floating point suffixes apply only to numbers, not booleans */
if ((offset <= len - 4 && s.regionMatches(true, offset, "true", 0, 4))
|| (offset <= len - 5 &&
s.regionMatches(true, offset, "false", 0, 5)))
return SqlParseUtil.parseTaintedValue(s, offset, this);
/* Parse the number up until the suffix. If that succeeds, and
* the suffix is OK (or if the suffix is not present), then the number
* is valid.
*/
int suffixOffset = -1;
Taint t = s.@internal@taint();
for (int i = offset + 1; i < len && t.get(i); i++) {
if (s.charAt(i) == 'f' || s.charAt(i) == 'F' ||
s.charAt(i) == 'd' || s.charAt(i) == 'D') {
suffixOffset = i;
break;
}
}
if (suffixOffset < 0)
return SqlParseUtil.parseTaintedValue(s, offset, this);
if (suffixOffset < len - 1 && t.get(suffixOffset + 1))
SqlUtil.abortQuery(s, "Tainted identifier, operator, or keyword");
/* Ensure that the number between offset and suffixOffset is a safe
* and valid tainted number
*/
SqlParseUtil.parseTaintedValue(s.substring(0, suffixOffset), offset,
this);
return suffixOffset;
}
public void validateSqlQuery(String s)
{
if (!s.@internal@isTainted()) return;
Taint t = s.@internal@taint();
int len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (t.get(i)) {
i = parseOracleTaintedValue(s, i, binaryFloatingPointNumbers);
continue;
}
switch(c) {
case '-':
/* Possibly a line comment */
if (i < len - 1 && s.charAt(i+1) == '-')
i = SqlParseUtil.parseLineComment(s, i, "--");
break;
case '/':
/* Possibly a block comment */
if (i < len - 1 && s.charAt(i+1) == '*')
i = SqlParseUtil.parseBlockComment(s, i);
break;
case '\'':
/* String Literal */
i = parseOracleString(s, i);
break;
case '"':
/* Quoted identifier */
i = parseOracleIdentifier(s, i);
break;
default:
break;
}
}
}
public OracleSqlValidator(Connection c, int dbMajor, int dbMinor) {
this.binaryFloatingPointNumbers = dbMajor >= 10;
this.quoteDelimitedStrings = dbMajor >= 10;
}
public OracleSqlValidator(boolean binaryFloatingPointNumbers,
boolean quoteDelimitedStrings) {
this.binaryFloatingPointNumbers = binaryFloatingPointNumbers;
this.quoteDelimitedStrings = quoteDelimitedStrings;
}
}