/* * 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; public final class SqlParseUtil { /** Parse a block comment. Nesting is supported. Tainted data is allowed * within the comment, but may neither begin nor end a block comment or * nested block comment. */ public static int parseNestedBlockComment(String s, int offset) { int len = s.length(), level = 1; Taint t = s.@internal@taint(); if (t.get(offset) || t.get(offset+1)) SqlUtil.abortQuery(s, "Tainted comment begin (/*)"); /* Skip leading '/*' */ offset++; while (level != 0 && ++offset < len) { char c = s.charAt(offset); if (c == '/' && offset != len-1 && s.charAt(offset + 1) == '*') { if (t.get(offset) || t.get(offset+1)) SqlUtil.abortQuery(s, "Tainted comment begin (/*)"); offset++; level++; } else if (c == '*' && offset != len-1 && s.charAt(offset + 1) == '/') { if (t.get(offset) || t.get(offset + 1)) SqlUtil.abortQuery(s, "Tainted comment end (*/)"); offset++; level--; } } if (level != 0) SqlUtil.parseError(s, "Unterminated block comment"); return offset; } /** Parse a block comment. Nesting is not supported. Tainted data is allowed * within the comment, but may neither begin nor end the comment. */ public static int parseBlockComment(String s, int offset) { int len = s.length(); Taint t = s.@internal@taint(); if (t.get(offset) || t.get(offset+1)) SqlUtil.abortQuery(s, "Tainted comment begin (/*)"); /* Skip leading '/*' */ offset++; while (++offset < len) { char c = s.charAt(offset); if (c == '*' && offset != len-1 && s.charAt(offset + 1) == '/') { if (t.get(offset) || t.get(offset + 1)) SqlUtil.abortQuery(s, "Tainted comment end (*/)"); return offset + 1; } } SqlUtil.parseError(s, "Unterminated block comment"); return offset; } /** Parse a line comment. Tainted input is allowed within the comment, but * must neither begin nor end the line comment. */ public static int parseLineComment(String s, int offset, String beginComment) { int len = s.length(), cmtlen = beginComment.length(); Taint t = s.@internal@taint(); for (int i = 0; i < cmtlen; i++) if (t.get(offset + i)) SqlUtil.abortQuery(s, "Tainted line comment begin(" + beginComment + ")"); /* Skip leading begin comment characters */ offset += cmtlen - 1; while (++offset < len) { char c = s.charAt(offset); if (c != '\r' && c != '\n') continue; else if (!t.get(offset)) return offset; else SqlUtil.abortQuery(s, "Tainted newline ends line comment"); } SqlUtil.parseError(s, "Unterminated line comment"); return offset; /* Allow comment to be terminated by end of query */ } public static int parseStringLiteral(String s, int offset, boolean escapes) { int len = s.length(); Taint t = s.@internal@taint(); char quoteChar = s.charAt(offset); 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 != quoteChar && (!escapes || c != '\\')) continue; if (c == quoteChar) { if (t.get(offset)) { /* Two single quotes interpreted as a literal quote */ if (offset < len-1 && s.charAt(offset+1) == quoteChar && t.get(offset+1)) offset++; else SqlUtil.abortQuery(s, "Unsafe tainted quote in string"); } else { if (offset == len-1 || s.charAt(offset+1) != quoteChar) return offset; if (t.get(offset+1)) SqlUtil.abortQuery(s, "Unsafe Tainted quote in string"); else offset++; } } else /* c == '\\' */ { if (t.get(offset)) { if (offset < len-1 && t.get(offset+1)) offset++; else SqlUtil.abortQuery(s, "Unsafe tainted backslash"); } else offset++; } } SqlUtil.parseError(s, "Unterminated string literal"); return offset; } public static int parseQuotedIdentifier(String s, int offset) { int len = s.length(); Taint t = s.@internal@taint(); char quoteChar = s.charAt(offset); while (++offset < len) { char c = s.charAt(offset); if (t.get(offset)) SqlUtil.abortQuery(s, "Tainted identifier"); /* A string ends with a double-quote. A literal double quote * is encoded as "". Thus, if we have an untainted " followed * by an untainted character that is not ", or an untainted " * followed by the end of the query, we have reached the end of * this string. */ if (c != quoteChar) continue; if (offset == len - 1 || s.charAt(offset+1) != quoteChar) return offset; if (t.get(offset+1)) SqlUtil.abortQuery(s, "Tainted identifier"); else /* "" is a literal " */ offset++; } SqlUtil.parseError(s, "Unterminated quoted identifier"); return offset; } /** Parse a tainted value -- which must be a numeric or boolean 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. * @param s query String * @param offset current position of the lexer * @return last character (inclusive) of tainted number or -1. * * The input format is (in BNF notation): * true (in any combination of upper or lowercase) * | false (in any combination of upper or lowercase) * | [+-] ({digit}+ | ({digit}+.{digit}*) | ({digit}*.{digit}+)) [(e|E)[+|-] * {digit}+] * Leading and trailing (potentially tainted) whitespace is allowed. */ public static int parseTaintedValue(String s, int offset, SqlValidator v) { final int /* Initial state */ ST_INITIAL = 0, /* After parsing (optional) leading + or - */ ST_INITIAL_POSTSIGN = 1, /* When parsing (one or more) leading digits */ ST_LEAD_DIGITS = 2, /* After parsing a decimal point. Next character must * be a digit for the number to be valid */ ST_DECIMAL_INVALID = 3, /* After parsing a decimal point, current parsed * string is (so far) a valid number */ ST_DECIMAL_VALID = 4, /* After parsing e|E and an optional + or - */ ST_EXPONENT_INVALID = 5, /* After parsing one or more trailing digits following the * exponent */ ST_EXPONENT_VALID = 6; int state = ST_INITIAL, len = s.length(); boolean fail = false; Taint t = s.@internal@taint(); /* Ensure that a numeric literal found at the current offset * will actually be parsed as a numeric literal. */ if (offset != 0 && !v.isSqlWhitespace(s.charAt(offset - 1)) && !v.isSqlOperator(s.charAt(offset - 1)) && !v.isSqlSpecial(s.charAt(offset - 1))) SqlUtil.abortQuery(s, "Tainted identifier, operator, or keyword"); if (offset <= len - 4) { int matchLen = 0; /* Check if true or false boolean constant, any case */ if (s.regionMatches(true, offset, "true", 0, 4)) matchLen = 4; else if (offset <= len - 5 && s.regionMatches(true, offset, "false", 0, 5)) matchLen = 5; for (int i = 0; i < matchLen; i++) if (!t.get(offset + i)) SqlUtil.abortQuery(s, "Partially tainted boolean constant"); if (matchLen != 0) return offset + matchLen - 1; } do { char c = s.charAt(offset); switch(state) { case ST_INITIAL: if (c == '+' || c == '-') { state = ST_INITIAL_POSTSIGN; break; } /* else fall through */ ; case ST_INITIAL_POSTSIGN: if (c >= '0' && c <= '9') state = ST_LEAD_DIGITS; else if (c == '.') state = ST_DECIMAL_INVALID; else fail = true; break; case ST_LEAD_DIGITS: if (c == '.') state = ST_DECIMAL_VALID; else if (c == 'e' || c == 'E') { if (offset < len - 1 && (s.charAt(offset+1) == '+' || s.charAt(offset+1) == '-')) offset++; state = ST_EXPONENT_INVALID; } else if (c < '0' || c > '9') fail = true; break; case ST_DECIMAL_INVALID: if (c >= '0' && c <= '9') state = ST_DECIMAL_VALID; else fail = true; break; case ST_DECIMAL_VALID: if (c == 'e' || c == 'E') { if (offset < len - 1 && (s.charAt(offset+1) == '+' || s.charAt(offset+1) == '-')) offset++; state = ST_EXPONENT_INVALID; } else if (c < '0' || c > '9') fail = true; break; case ST_EXPONENT_INVALID: if (c < '0' || c > '9') fail = true; else state = ST_EXPONENT_VALID; break; case ST_EXPONENT_VALID: /* Terminal state */ if (c < '0' || c > '9') fail = true; break; default: throw new RuntimeException("switch"); } } while (++offset < len && t.get(offset) && !fail); if (state == ST_INITIAL || state == ST_INITIAL_POSTSIGN || state == ST_DECIMAL_INVALID || state == ST_EXPONENT_INVALID || fail) SqlUtil.abortQuery(s, "Tainted identifier, operator, or keyword"); return offset - 1; } }