/* * 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.util.Random; import java.util.List; import java.util.ArrayList; import java.util.BitSet; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.sql.SQLException; public class PostgreSqlValidatorTest { private final int maxlen; private final SafeRandom sr; private final boolean standardConformingStrings; private final boolean nestedBlockComments; private final boolean dollarQuotedStrings; private final PostgreSqlValidator v; private boolean exception; public PostgreSqlValidatorTest(int maxlen, SafeRandom sr, boolean standardConformingStrings, boolean nestedBlockComments, boolean dollarQuotedStrings) { this.maxlen = maxlen; this.sr = sr; this.standardConformingStrings = standardConformingStrings; this.nestedBlockComments = nestedBlockComments; this.dollarQuotedStrings = dollarQuotedStrings; this.v = new PostgreSqlValidator(standardConformingStrings, nestedBlockComments, dollarQuotedStrings); clearException(); } /* Can't assume enum support -- so we're going to do this the hard way */ static class QueryToken { static final int Q_SINGLELIT = 0; /* String Literal - 'foo' */ static final int Q_DOLLARLIT = 1; /* String Literal - '$$foo$$' */ static final int Q_QUOTEID = 2; /* Quoted Identifier - "foo" */ static final int Q_NUMBER = 3; /* Number */ static final int Q_LINECOMM = 4; /* Line Comment */ static final int Q_BLKCOMM = 5; /* Block Comment */ static final int Q_OTHER = 6; /* Operators, Identifiers, etc. */ static final int Q_BOOLEAN = 7; /* Boolean Literal */ static final int Q_END = 8; static String[] qMethod = { "SingleQuoteLiteral", "DollarLiteral", "QuotedIdentifier", "Number", "LineComment", "BlockComment", "Other", "Boolean" }; static String qChars = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789`';,/[]-=~!@#$%^&*()_+{}:\"/,.<>?"; public static char randChar(SafeRandom sr) { return qChars.charAt(sr.nextInt(qChars.length())); } } public boolean pendingException() { return exception; } public void clearException() { exception = false; } public void setException() { exception = true; } private void appendSingleQuoteLiteral(@StringBuilder@ query) { int len = sr.nextInt(256); boolean esc = false; /* backslash escape seen */ boolean scStrings = standardConformingStrings; StringBuffer sb = new StringBuffer(len); BitSet b = new BitSet(); String s; char qc = ' '; if (query.length() > 0) qc = query.charAt(query.length() - 1); if (qc == '\'') sb.append(' '); if (sr.nextBoolean()) { if (!v.isSqlWhitespace(qc) && !v.isSqlOperator(qc) && !v.isSqlSpecial(qc)) sb.append(' '); if (sr.nextBoolean()) sb.append('E'); else sb.append('e'); scStrings = false; } sb.append('\''); for (int i = 0; i < sb.length(); i++) { if (sr.nextInt(256) == 0) { b.set(i); setException(); } } for (int i = sb.length(); esc || i < len + 1; i++) { int oldlen = sb.length(); boolean taint = sr.nextBoolean(); int c; int clen; if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] if (c == '\0' && taint) setException(); if (c == '\'' && !esc) { if (!taint) { sb.append('\''); if (sr.nextInt(256) == 0) { b.set(oldlen+1); setException(); } } else { sb.append('\''); if (sr.nextInt(256) != 0) { b.set(oldlen+1); } else { setException(); } } } else if (c == '\\' && !scStrings && !esc) { esc = true; } else if (esc) { if (b.get(oldlen-1) && !taint) setException(); esc = false; } //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); } sb.append('\''); if (sr.nextInt(256) == 0) { b.set(sb.length() - 1); setException(); } s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); } /* We can't use enums due to Java 1.4 support. These are the states * used by the state machine in appendBlockComment */ private static final int DEFAULT = 0, SEEN_SLASH = 1, SEEN_STAR = 2; private void appendBlockComment(@StringBuilder@ query) { int len = sr.nextInt(64); StringBuffer sb = new StringBuffer(len); String s; BitSet b = new BitSet(); int level = 1, state = DEFAULT; sb.append("/*"); for (int i = 0; level > 0 && i < len; i++) { int oldlen = sb.length(); boolean taint = sr.nextBoolean(); int c; int clen; int nextState; if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] if (c == '*') nextState = SEEN_STAR; else if (c == '/') nextState = SEEN_SLASH; else nextState = DEFAULT; if (nestedBlockComments && state == SEEN_SLASH && c == '*') { level++; if (taint || b.get(oldlen-1)) setException(); nextState = DEFAULT; } else if (state == SEEN_STAR && c == '/') { level--; if (taint || b.get(oldlen-1)) setException(); nextState = DEFAULT; } //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); state = nextState; } for (int i = 0; i < level; i++) sb.append(" */"); s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); } private void appendLineComment(@StringBuilder@ query) { int i, len = 1 + sr.nextInt(64); StringBuffer sb = new StringBuffer(len); BitSet b = new BitSet(); String s; sb.append("--"); for (i = 0; i < len; i++) { int oldlen = sb.length(); boolean taint = sr.nextBoolean(); int c; int clen; if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] if (taint && (c == '\r' || c == '\n')) setException(); //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); if (c == '\r' || c == '\n') break; } if (i == len) { if (sr.nextBoolean()) sb.append('\r'); else sb.append('\n'); } s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); } private void appendBoolean(@StringBuilder@ query) { String s; BitSet b = new BitSet(); boolean taint = sr.nextBoolean(); int oldlen = query.length(); if (oldlen != 0 && !v.isSqlWhitespace(query.charAt(oldlen - 1)) && !v.isSqlOperator(query.charAt(oldlen - 1)) && !v.isSqlSpecial(query.charAt(oldlen - 1)) && taint) { if (sr.nextInt(256) == 0) setException(); else { query.append(' '); } } if (sr.nextBoolean()) s = "true"; else s = "false"; @StringBuilder@ sb = new @StringBuilder@(s.length()); for (int i = 0; i < s.length(); i++) { if (sr.nextInt(128) == 0) { /* Create a partially tainted boolean literal */ if (!taint) b.set(i); } else if (taint) { b.set(i); } if (sr.nextBoolean()) sb.append(Character.toUpperCase(s.charAt(i))); else sb.append(s.charAt(i)); } sb.append(' '); /* In case we are followed by a string literal, an * E or e could be misinterpreted as an escape string */ /* Partially tainted boolean literals are forbidden */ if (b.cardinality() != 0 && b.cardinality() != s.length()) setException(); query.append(new String(sb.toString(), new Taint(b, sb.length()))); } private void appendOther(@StringBuilder@ query) { int len = 1 + sr.nextInt(64), origLength = query.length(); StringBuffer sb = new StringBuffer(len); BitSet b = new BitSet(); boolean dolvalid = false; String s; for (int i = 0; i < len; i++) { int oldlen = sb.length(); boolean taint = sr.nextInt(64) == 0; int c, clen; char prevc; if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] /* Ensure we aren't creating a boolean literal */ String q = sb.toString(); if ((q.regionMatches(true, sb.length() - 3, "tru", 0, 3) || q.regionMatches(true, sb.length() - 4, "fals", 0, 4)) && (c == 'e' || c == 'E')) { i--; continue; } if (sb.length() > 0) prevc = sb.charAt(sb.length()-1); else if (query.length() > 0) prevc = query.charAt(query.length()-1); else prevc = ' '; /* Untainted ' or " are handled by SingleQ and QuoteID functions */ if (c == '\'' || c == '"') { i--; continue; } /* Similarly, untainted '/' or '*' that result in comments must * be avoided and handled by the LineComment/BlockComment fns */ else if (prevc == '*' && c == '/') { i--; continue; } else if (prevc == '/' && c == '*') { i--; continue; } /* Do not create a line comment */ else if (prevc == '-' && c == '-') { i--; continue; } /* Do not create a valid dollar tag */ else if (dolvalid && c == '$') { i--; continue; } //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); /* Update dolvalid - true if we are within a valid (potential) * dollar-identifier tag. */ if (c == '$' && dollarQuotedStrings) dolvalid = true; else if (dolvalid) { for (int j = oldlen; j < sb.length(); j++) { if (dolvalid && j > 0 && sb.charAt(j-1) == '$' && !PostgreSqlValidator.isPGDollarStart(sb.charAt(j))) dolvalid = false; else if(dolvalid && !PostgreSqlValidator.isPGDollarCont(sb.charAt(j))) dolvalid = false; } } } char lastc = sb.charAt(sb.length() - 1); /* Ensure that we do not end in a valid dollar tag or comment, or * a tainted string literal */ if (dolvalid || lastc == '/' || lastc == '*' || lastc == '-' || b.get(sb.length()-1) || lastc == 'E' || lastc == 'e' || (lastc >= '0' && lastc <= '9')) sb.append(' '); s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); if (!s.@internal@isTainted()) return; /* Any tainted character will cause a security exception unless all * tainted substrings are valid numeric literals */ for (int offset = origLength; offset < query.length(); offset++) { int toff = 0; if (!b.get(offset-origLength)) continue; if (pendingException()) break; try { toff = SqlParseUtil.parseTaintedValue(query.toString(),offset,v); } catch (JTaintException e) { setException(); } if (toff > 0) offset = toff; } } private void appendQuotedIdentifier(@StringBuilder@ query) { int len = sr.nextInt(64); StringBuffer sb = new StringBuffer(len+2); BitSet b = new BitSet(); String s; sb.append('"'); for (int i = 1; i < len + 1; i++) { int c, clen; int oldlen = sb.length(); boolean taint; if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] taint = sr.nextInt(1024) == 0; if (taint) setException(); if (c == '"') sb.append('"'); //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); } sb.append('"'); sb.append(" "); /* If the next Token begins with a tainted '"', avoid * a security exception */ s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); } private void appendNumber(@StringBuilder@ query) { String digit = "0123456789"; int len = 1 + sr.nextInt(32), oldlen = query.length(); StringBuffer sb = new StringBuffer(); boolean taint = sr.nextBoolean(); String s; if (oldlen != 0 && !v.isSqlWhitespace(query.charAt(oldlen - 1)) && !v.isSqlOperator(query.charAt(oldlen - 1)) && !v.isSqlSpecial(query.charAt(oldlen - 1)) && taint) { if (sr.nextInt(256) == 0) setException(); else { query.append(' '); oldlen++; } } if (sr.nextBoolean()) { if (sr.nextBoolean()) sb.append('+'); else sb.append('-'); } if (sr.nextBoolean()) { /* leading digits */ for (int i = 0; i < len; i++) { sb.append(digit.charAt(sr.nextInt(digit.length()))); } if (sr.nextBoolean()) { sb.append('.'); if (sr.nextBoolean()) { int len2 = sr.nextInt(32); for (int j = 0; j < len2; j++) { sb.append(digit.charAt(sr.nextInt(digit.length()))); } } } } else { sb.append('.'); for (int i = 0; i < len; i++) sb.append(digit.charAt(sr.nextInt(digit.length()))); } if (sr.nextBoolean()) { int len3 = 1 + sr.nextInt(32); if (sr.nextBoolean()) sb.append('e'); else sb.append('E'); if (sr.nextBoolean()) { if (sr.nextBoolean()) sb.append('+'); else sb.append('-'); } for (int k = 0; k < len3; k++) sb.append(digit.charAt(sr.nextInt(digit.length()))); } sb.append(' '); Taint t = new Taint(taint, sb.length()); t.clear(sb.length()-1); s = new String(sb.toString(), t); query.append(s); } private String randDollarTag() { int len = sr.nextInt(64); StringBuffer sb = new StringBuffer(len+2); sb.append('$'); for (int i = 0; i < len; i++) { boolean found = false; char c; /* Find a valid Dollar tag character */ do { c = (char) sr.nextInt(256); /* First char is treated differently */ if (i == 0) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c > 127 && c < 256)) found = true; } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || (c > 127 && c < 256) || (c >= '0' && c <= '9')) { found = true; } } while (!found); sb.append(c); } sb.append('$'); return sb.toString(); } private void appendDollarQuoteLiteral(@StringBuilder@ query) { int taglen, len = sr.nextInt(256); StringBuffer sb = new StringBuffer(len+2); BitSet b = new BitSet(); String s, tag; boolean match = false; /* true if created terminating tag randomly */ /* Append tag */ tag = randDollarTag(); taglen = tag.length(); sb.append(tag); for (int i = 0; i < taglen; i++) { if (sr.nextInt(256) == 0) { b.set(i); setException(); } } /* Append random characters, terminating if we accidentally append * $tag$ */ for (int i = taglen; i < len + taglen && !match; i++) { int c, clen; int oldlen = sb.length(), newlen; boolean taint = sr.nextBoolean(); if (sr.nextInt(12) == 0) //[ifJava5+] c = Character.MIN_CODE_POINT + sr.nextInt( Character.MAX_CODE_POINT - Character.MIN_CODE_POINT); //[fiJava5+] //[ifJava4] c = sr.nextInt(0xffff + 1); //[fiJava4] else c = QueryToken.randChar(sr); //[ifJava5+] clen = Character.charCount(c); //[fiJava5+] //[ifJava4] clen = 1; //[fiJava4] //[ifJava5+] sb.append(Character.toChars(c)); //[fiJava5+] //[ifJava4] sb.append((char)c); //[fiJava4] for (int j = 0; j < clen; j++) b.set(oldlen + j, taint); newlen = sb.length(); if (c == '\0' && taint) setException(); /* Did we terminate our dollar identifier randomly? */ if (c == '$') { if (newlen >= taglen *2 && sb.substring(newlen - taglen, newlen).equals(tag)) { match = true; for (int j = newlen - taglen; j < newlen; j++) if (b.get(j)) setException(); } } } /* Append final tag */ if (!match) { int oldlen; sb.append(' '); /* in case we ended on a '$' in previous loop */ oldlen = sb.length(); sb.append(tag); for (int i = oldlen; i < oldlen + taglen; i++) if (sr.nextInt(256) == 0) { b.set(i); setException(); } } s = new String(sb.toString(), new Taint(b, sb.length())); query.append(s); } public void appendRandomToken(@StringBuilder@ query, List opList) { int op = sr.nextInt(QueryToken.Q_END); if (opList != null) opList.add(QueryToken.qMethod[op]); switch(op) { case QueryToken.Q_SINGLELIT: appendSingleQuoteLiteral(query); break; case QueryToken.Q_DOLLARLIT: if (dollarQuotedStrings) appendDollarQuoteLiteral(query); break; case QueryToken.Q_QUOTEID: appendQuotedIdentifier(query); break; case QueryToken.Q_NUMBER: appendNumber(query); break; case QueryToken.Q_LINECOMM: appendLineComment(query); break; case QueryToken.Q_BLKCOMM: appendBlockComment(query); break; case QueryToken.Q_OTHER: appendOther(query); break; case QueryToken.Q_BOOLEAN: appendBoolean(query); break; default: throw new RuntimeException("switch"); } } public void test() { int len = 1 + sr.nextInt(maxlen-1); List opList = new ArrayList(); @StringBuilder@ query = new @StringBuilder@(); clearException(); while(query.length() < len) { @StringBuilder@ oldQuery = new @StringBuilder@(query.toString()); Throwable e = null; appendRandomToken(query, opList); try { v.validateSqlQuery(query.toString()); } catch (JTaintException s) { if (pendingException()) { clearException(); if (query.toString().indexOf(0) != -1) Log.clearWarning(); query = oldQuery; opList.add("Exception"); continue; } else { e = s; } } catch(Throwable th) { e = th; } /* Suppress null byte in SQL query warnings */ if (query.toString().indexOf(0) != -1) Log.clearWarning(); if (e != null || pendingException()) { String[] ops = new String[opList.size()]; opList.toArray(ops); System.out.println("FAILURE-- query: " + query); System.out.println("Taint " + query.toString().@internal@taint().toString()); System.out.println("Standard conforming strings: " + standardConformingStrings); System.out.println("Nested block comments: " + nestedBlockComments); System.out.println("Dollar quoted Strings: " + dollarQuotedStrings); for (int j = 0; j < ops.length; j++) System.out.println("op " + j + ": " + ops[j]); if (pendingException()) System.out.println("FAILURE: did not get exception"); else { System.out.println("FAILURE " + e); e.printStackTrace(); } System.exit(-1); } } } public static void main(String[] args) { long seed = System.currentTimeMillis(); int maxlen = 65536; int nrtest = 16384; SafeRandom sr; PostgreSqlValidatorTest qt; String logfile = "PostgreSqlValidatorTest.log"; PrintStream ps = null; for (int i = 0; i < args.length; i++) { if (args[i].equals("-s")) seed = Long.decode(args[++i]).longValue(); else if (args[i].equals("-l")) maxlen = Integer.decode(args[++i]).intValue(); else if (args[i].equals("-n")) nrtest = Integer.decode(args[++i]).intValue(); else if (args[i].equals("-f")) logfile = args[++i]; else { System.out.println("Usage: java PostgreSqlValidatorTest " + "[-s randomSeed] " + "[-l maximumLengthofQuery] " + "[-n NumberofTests]" + "[-f logFileName]"); System.exit(-1); } } try { ps = new PrintStream(new FileOutputStream(logfile)); } catch (FileNotFoundException e) { System.out.println("Error opening logfile [" + logfile + "]: " + e); System.exit(-1); } ps.print("-s "); ps.print(seed); ps.print(" -l "); ps.print(maxlen); ps.print(" -n "); ps.print(nrtest); ps.print(" -f "); ps.print(logfile + "\n"); ps.flush(); ps.close(); sr = new SafeRandom(seed); qt = new PostgreSqlValidatorTest(maxlen, sr, sr.nextBoolean(), sr.nextBoolean(), sr.nextBoolean()); for (int i = 0; i < nrtest; i++) qt.test(); if (Log.hasError() || Log.hasWarning() || Log.hasVuln()) { System.out.println("Error or Warning encountered -- check logs"); System.exit(-1); } } }