/*
* 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;
import java.sql.ResultSet;
import java.sql.Statement;
/*
Postgresql - supporting 7.0 - 8.3
Lexical changes between 7.0 and 8.3:
Feature: Nested Block Comments
Added: 7.1
Feature: Dollar quoted String Constants
Added: 8.0
Feature: escape strings (and standardconformingstrings configuration param)
Added: 8.1
*/
public final class PostgreSqlValidator extends SqlValidator
{
private final boolean standardConformingStrings;
private final boolean nestedBlockComments;
private final boolean dollarQuotedStrings;
public boolean isSqlOperator(char c) {
return "_@#`~!()[]{},.;:+-*/%&<>=|^?".indexOf(c) != -1;
}
public boolean isSqlSpecial(char c) {
return "$'\"".indexOf(c) != -1;
}
public boolean isSqlWhitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f';
}
public int parsePGString(String s, int offset, boolean scStrings)
{
int len = s.length();
Taint t = s.@internal@taint();
boolean isStandardString = scStrings;
/* Check for 'Escape string' syntax */
if (isStandardString && offset >= 1
&& (s.charAt(offset-1) == 'e' || s.charAt(offset-1) == 'E')) {
if (t.get(offset-1))
SqlUtil.abortQuery(s, "Tainted escape string specifier");
else if (offset == 1 || isSqlWhitespace(s.charAt(offset - 2))
|| isSqlOperator(s.charAt(offset - 2))
|| isSqlSpecial(s.charAt(offset - 2)))
isStandardString = false; /* Found escape string */
}
return SqlParseUtil.parseStringLiteral(s, offset, !isStandardString);
}
public static boolean subStringEqual(final String s, final int offA,
final int offB, final int len)
{
if (offA < 0 || offB < 0
|| offA >= s.length() || offB >= s.length()
|| offA + len > s.length() || offB + len > s.length())
return false;
for (int i = 0; i < len; ++i)
{
if (s.charAt(offA + i) != s.charAt(offB + i))
return false;
}
return true;
}
public static boolean isPGDollarStart(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| c == '_' || (c > 127 && c < 256);
}
public static boolean isPGDollarCont(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|| c == '_' || (c > 127 && c < 256)
|| (c >= '0' && c <= '9');
}
public int parsePGDollarString(String s, int offset)
{
Taint t = s.@internal@taint();
int len = s.length();
int tagLen, i;
if (s.charAt(offset + 1) == '$')
tagLen = 2;
else
tagLen = s.indexOf('$', offset + 1) - offset + 1;
for (i = offset + tagLen; i < len; i++)
{
if (s.charAt(i) == '\0') {
if (t.get(i))
SqlUtil.abortQuery(s, "Tainted null byte in dollar string");
Log.warn(new Throwable("Null byte in SQL Query: " + s));
}
if (s.charAt(i) == '$' && subStringEqual(s, offset, i, tagLen)) {
if (s.substring(i, i + tagLen).@internal@isTainted()) {
SqlUtil.abortQuery(s,
"Tainted character terminates dollar string");
}
return i + tagLen - 1;
}
}
SqlUtil.parseError(s, "Unterminated dollar literal");
return i;
}
public void validateSqlQuery(String s)
{
int len;
Taint t;
if (!s.@internal@isTainted()) return;
t = s.@internal@taint();
len = s.length();
for (int i = 0; i < len; i++) {
char c = s.charAt(i);
if (t.get(i)) {
i = SqlParseUtil.parseTaintedValue(s, i, this);
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) == '*') {
if (nestedBlockComments)
i = SqlParseUtil.parseNestedBlockComment(s, i);
else
i = SqlParseUtil.parseBlockComment(s, i);
}
break;
case '\'':
/* String Literal */
i = parsePGString(s, i, standardConformingStrings);
break;
case '"':
/* Quoted identifier */
i = SqlParseUtil.parseQuotedIdentifier(s, i);
break;
case '$':
{
/* Possibly a dollar literal */
boolean found = false,
unsafe = false;
if (dollarQuotedStrings && i < len - 1
&& (s.charAt(i+1) == '$'
|| isPGDollarStart(s.charAt(i+1)))) {
for (int d = i + 1; d < len; d++) {
if (t.get(d)) {
unsafe = true;
} else if (s.charAt(d) == '$') {
found = true;
break;
} else if (!isPGDollarCont(s.charAt(d)))
break;
}
if (found) {
if (unsafe)
SqlUtil.abortQuery(s, "Tainted dollar string");
i = parsePGDollarString(s, i);
break;
}
/* else fall through */
}
}
default:
break;
}
}
}
public PostgreSqlValidator(Connection c, int dbMajor, int dbMinor) {
if (dbMajor < 7 || (dbMajor == 7 && dbMinor < 1))
nestedBlockComments = false;
else
nestedBlockComments = true;
if (dbMajor < 8)
dollarQuotedStrings = false;
else
dollarQuotedStrings = true;
if (dbMajor < 8 || (dbMajor == 8 && dbMinor < 1)) {
standardConformingStrings = false;
} else {
boolean scStrings = false;
Statement st = null;
try {
String scQuery=
"select current_setting('standard_conforming_strings')";
st = c.createStatement();
ResultSet rs = st.executeQuery(scQuery);
if (rs.next() && rs.getString(1).equals("on"))
scStrings = true;
} catch (Throwable th) {
Log.error(th);
}
standardConformingStrings = scStrings;
if (st != null) {
try {
st.close();
} catch (Throwable th) { }
}
}
}
public PostgreSqlValidator(boolean standardConformingStrings,
boolean nestedBlockComments,
boolean dollarQuotedStrings)
{
this.standardConformingStrings = standardConformingStrings;
this.nestedBlockComments = nestedBlockComments;
this.dollarQuotedStrings = dollarQuotedStrings;
}
}