/* * 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.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.BitSet; import java.util.List; import java.util.Random; import java.util.StringTokenizer; public class FileTest { private final int maxlen; private final Random r; private boolean exception; private static String currentOp; private static String currentPath; private static String currentChild; private static File currentFile; private static URI currentURI; private static String exceptionMsg; public FileTest(int maxlen, Random r) { this.maxlen = maxlen; this.r = r; clearException(); } private void clearException() { exception = false; exceptionMsg = null; } private void setException(String msg) { exception = true; exceptionMsg = msg; } private boolean pendingException() { return exception; } private String randString() { int len = 1 + r.nextInt(64); @StringBuilder@ sb = new @StringBuilder@(len); BitSet b = new BitSet(); if (r.nextInt(128) == 0) { b.set(0, r.nextBoolean()); return new String(".", new Taint(b, 1)); } if (r.nextInt(4) == 0) { b.set(0, r.nextBoolean()); b.set(1, r.nextBoolean()); return new String("..", new Taint(b, 2)); } for (int i = 0; i < len; i++) { int c, clen; boolean taint = r.nextInt(64) == 0; int oldlen = sb.length(); c = 'a' + r.nextInt(26); if (c == '.' || c == '/') { i--; continue; } if (c == '\0') { if (r.nextInt(16) == 0) { i--; continue; } taint = true; } sb.append((char) c); b.set(oldlen, taint); } return new String(sb.toString(), new Taint(b, sb.length())); } private String randChar(char c, int taintP) { boolean taint = r.nextInt(taintP) == 0; return new String(String.valueOf(c), new Taint(taint, 1)); } static private String realPath(String path) { @StringBuilder@ sb = new @StringBuilder@(path.length()); for (int i = 0; i < path.length(); i++) { if (path.charAt(i) != '/') { sb.append(path.charAt(i)); continue; } /* Strip any redundant /'s */ do { i++; } while (i < path.length() && path.charAt(i) == '/'); i--; if (i == path.length() - 1 || path.charAt(i+1) != '.') { sb.append(path.charAt(i)); continue; } if (i == path.length() -2 || path.charAt(i+2) == '/') { /* pathname is /./ */ i+=1; continue; } else if (path.charAt(i+2) != '.') throw new RuntimeException("corrupt path: " + path); else { /* Pathname is /../ */ int lastSlash = sb.lastIndexOf("/"); if (lastSlash >= 0) sb.replace(lastSlash, sb.length(), ""); i += 2; } } if (sb.length() == 0) /* Occurs if we get a string like /.. */ return "/"; return sb.toString(); } static private String untaintedPrefix(String path) { int i; Taint t = path.@internal@taint(); if (t == null) t = new Taint(false, path.length()); for (i = 0; i < path.length(); i++) if (t.get(i)) break; return path.substring(0, i); } static private boolean isSafePath(String path) { String prefix; if (!path.@internal@isTainted()) return true; prefix = realPath(untaintedPrefix(path)); path = realPath(path); /* Handle a serious edge case: If the prefix is a directory that ends * in a trailing slash, and the tainted input is of the form dir/.., * then we need to allow both the prefix and the prefix sans trailing * slash as possible matches. */ if (prefix.length() > 0 && prefix.charAt(prefix.length()-1) == '/' && prefix.substring(0, prefix.length()-1).equals(path)) return true; return path.startsWith(prefix); } private String randPath() { int len = 1 + r.nextInt(maxlen); StringBuffer sb = new StringBuffer(len); boolean isAbsolute = r.nextBoolean(), foundTaint = false; String path, prefix; /* Ensure that our filename doesn't actually match a possible * symbolic link. For absolute paths, we reference the * the property test.abs.nosym. For relative paths, we already * set the value of the user.dir property to the value of the * test.rel.nosym property. The user must set test.rel.nosym and * test.abs.nosym.to the full absolute path of (possibly distinct) * directories containing no symbolic links. */ if (isAbsolute) { prefix = System.getProperty("test.abs.nosym"); BitSet b = new BitSet(); for (int i = 0; i < prefix.length(); i++) if (r.nextInt(128) == 0) b.set(i); prefix = new String(prefix, new Taint(b, prefix.length())); sb.append(prefix); } else { do { prefix = randString(); } while (prefix.charAt(0) == '.'); sb.append(prefix + randChar('/', 128)); } while (sb.length() < len) { String s = randString(); /* XXX Fun note: * There is currently a bug in Java where File.getCanonicalPath() * fails to actually strip '..' in very limited circumstances. * This only occurs when the _untainted prefix_ tries to reference * a directory _below_ the root directory, and the pathname * contains a directory that does not exist. For example * new File("/foo/../../bin/ls").getCanonicalPath() * returns /../bin/ls if /foo does not exist. Bug submitted to Sun. * For now, we don't allow workaround this the old fashioned * way. */ if (s.charAt(0) == '.' && "/".equals(realPath(toAbsolutePath(sb.toString())))) continue; sb.append(s); if (sb.length() < len || (sb.length() >= len && r.nextBoolean())) { int nslash = 1 + r.nextInt(7); sb.append(randChar('/', 64)); } } return sb.toString(); } private String toAbsolutePath(String pathname) { if (pathname.charAt(0) == '/') return pathname; return System.getProperty("user.dir") + pathname; } private void validatePath(String pathname) { pathname = toAbsolutePath(pathname); Taint t = pathname.@internal@taint(); if (t == null) t = new Taint(false, pathname.length()); if (!isSafePath(pathname)) setException("Pathname " + pathname + " is unsafe"); if (t.get(0)) setException("Tainted absolute path"); else if (pathname.@internal@isTainted() && realPath(untaintedPrefix(pathname)).equals("/")) setException("Tainted path confined to root directory"); for (int i = 0; i < pathname.length(); i++) if (pathname.charAt(i) == '\0' && t.get(i)) setException("Tainted null byte"); } private String randValidDirectory() { String s; do { clearException(); s = randPath(); if (s.charAt(s.length() - 1) != '/') s = s.substring(0, s.lastIndexOf('/') + 1); validatePath(s); } while(pendingException()); return s; } private void testStringParent() { String parent = randValidDirectory(); String child = randString(); currentChild = child; if (r.nextInt(128) == 0) { parent = ""; currentPath = parent; validatePath("/" + child); if (child.@internal@isTainted()) setException("File with empty parent and tainted child"); new File(parent, child); return; } else if(r.nextInt(128) == 0) { parent = null; currentPath = parent; validatePath(child); new File(parent, child); return; } currentPath = parent; validatePath(parent + child); new File(parent, child); } private void testFileParent() { String parent = randValidDirectory(); String child = randString(); currentChild = child; if (r.nextInt(128) == 0) { parent = ""; currentPath = parent; validatePath("/" + child); if (child.@internal@isTainted()) setException("File with empty parent and tainted child"); new File(new File(parent), child); return; } else if(r.nextInt(128) == 0) { parent = null; currentPath = parent; validatePath(child); new File((File)null, child); return; } currentPath = parent; validatePath(parent + child); new File(new File(parent), child); } private void testPath() { String pathname = randPath(); currentPath = pathname; validatePath(pathname); new File(pathname); } private void testURI() throws URISyntaxException { URI uri; String pathname = randPath(); String scheme = "file"; BitSet b = new BitSet(); /* URI paths must be absolute*/ pathname = toAbsolutePath(pathname); for (int i = 0; i < scheme.length(); i++) if (r.nextInt(256) == 0) { b.set(i); setException("Tainted URI scheme"); } scheme = new String(scheme, new Taint(b, scheme.length())); currentPath = pathname; validatePath(pathname); uri = new URI(scheme, /* authority */ null, pathname, /* query */ null, /* fragment */ null); new File(uri); } private void testSerialize() throws IOException, ClassNotFoundException { File f = new File(randValidDirectory()); String canonPath = f.getCanonicalPath(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream os = new ObjectOutputStream(bout); os.writeObject(f); os.flush(); os.close(); bout.close(); ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); ObjectInputStream is = new ObjectInputStream(bin); f = (File) is.readObject(); is.close(); bin.close(); if (!canonPath.equals(f.getCanonicalPath()) || f.@internal@untaintedPrefix() != null) throw new RuntimeException("Corrupted serialization"); } private void testSymlink() { String prefix = System.getProperty("test.sym"); String[] badFiles = { "t1/t2/bad1", "t1/t2/bad2", "t1/t2/bad3", "t1/t2/bad1/bob/a/f/g", "t1/t2/bad2/foo.txt", "t1/t2/bad4" }; String[] goodFiles = { "t1/t2/good1", "t1/t2/good2", "t1/t2/good3", "t1/good1/foo.txt", "t1/t2/good4", "t1/t2/good5", "t1/t2/good5/t1/t2/good1" }; /* Initialize tainted suffixes */ for (int i = 0; i < badFiles.length; i++) badFiles[i] = new String(badFiles[i], new Taint(true, badFiles[i].length())); for (int i = 0; i < goodFiles.length; i++) goodFiles[i] = new String(goodFiles[i], new Taint(true, goodFiles[i].length())); /* Test BadFiles */ for (int i = 0; i < badFiles.length; i++) { boolean caught = false; try { new File(prefix + badFiles[i]); } catch (JTaintException e) { caught = true; } if (!caught) throw new RuntimeException("Expected exception for symlink " + prefix + badFiles[i]); } /* Test GoodFiles */ for (int i = 0; i < goodFiles.length; i++) new File(prefix + goodFiles[i]); } private void test() throws IOException, URISyntaxException, ClassNotFoundException { currentOp = "symlink"; testSymlink(); currentPath = null; currentChild = null; currentURI = null; currentOp = null; switch(r.nextInt(5)) { case 0: currentOp = "File(String pathname)"; testPath(); break; case 1: currentOp = "File(String parent, String child)"; testStringParent(); break; case 2: currentOp = "File(File parent, String child)"; testFileParent(); break; case 3: currentOp = "File(URI uri)"; testURI(); break; case 4: currentOp = "Serialize(File)"; testSerialize(); break; default: throw new RuntimeException("switch"); } } public static void main(String[] args) { long seed = System.currentTimeMillis(); int maxlen = 128; int nrtest = 16384; Random r; FileTest ft; String logfile = "FileTest.log"; PrintStream ps = null; System.setProperty("user.dir", System.getProperty("test.rel.nosym")); if (System.getProperty("user.dir") == null || !System.getProperty("user.dir").equals( System.getProperty("test.rel.nosym"))) { System.out.println("Unable to set user.dir to test.nosym"); System.exit(-1); } if (System.getProperty("test.sym") == null) { System.out.println("Could not find test.sym"); System.exit(-1); } if (System.getProperty("test.abs.nosym") == null) { System.out.println("Could not find test.abs.nosym"); System.exit(-1); } 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 FileTest " + "[-s randomSeed] " + "[-l maximumLengthofCommand] " + "[-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(); r = new Random(seed); ft = new FileTest(maxlen, r); try { ft.testSymlink(); } catch(Exception e) { throw new RuntimeException("testValid: unexpected exception", e); } for (int i = 0; i < nrtest; i++) { Exception e = null; ft.clearException(); try { ft.test(); if (ft.pendingException()) e = new RuntimeException("Expected exception - got none"); } catch (JTaintException s) { if (!ft.pendingException()) e = new RuntimeException("Unexpected JTaintException", s); } catch (Throwable x) { e = new RuntimeException("Unexpected other exception", x); } if (e != null) { System.out.println("FAILURE -- Op " + currentOp); if (currentPath != null) { System.out.println("Current pathname: " + currentPath); System.out.println("Taint: " + currentPath.@internal@taint()); } if (currentChild != null) { System.out.println("Current child: " + currentChild); System.out.println("Taint: " + currentChild.@internal@taint()); } if (currentFile != null) { System.out.println("Current file: "+currentFile.getPath()); System.out.println("Untainted prefix: " + currentFile.@internal@untaintedPrefix()); } if (currentURI != null) { String query = currentURI.getQuery(); System.out.println("Current URI: " + currentURI); System.out.println("Query: " + query); System.out.println("Query Taint: " + query.@internal@taint()); } if (exceptionMsg != null) System.out.println("Expected exception: " + exceptionMsg); e.printStackTrace(); System.exit(-1); } } if (Log.hasError() || Log.hasWarning() || Log.hasVuln()) { System.out.println("Error or Warning encountered -- check logs"); System.exit(-1); } } }