/*
* 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.File;
import java.io.IOException;
import java.net.URI;
public final class FileUtil
{
private static final String[] fsRootPaths;
static {
File[] roots = File.listRoots();
int len = roots.length;
String[] tmp = new String[len];
for (int i = 0; i < len; i++)
tmp[i] = roots[i].getPath();
/* Assign fsRootPaths only after the array has been initialized.
* This is due to the memory consistency rules for final fields in
* the Java Memory Model.
*/
fsRootPaths = tmp;
}
private static void abortFile(String untaintedPrefix, File result,
String msg)
throws JTaintException
{
@StringBuilder@ sb = new @StringBuilder@();
JTaintException e;
try {
sb.append("\nPathname: " + result.getCanonicalPath());
} catch (Throwable th) {
Log.error(th);
}
if (untaintedPrefix != null)
sb.append("\nEscaped from directory " + untaintedPrefix + "\n");
e = new JTaintException(sb.toString(), Configuration.fileWhitelist);
if (Configuration.filePolicyLogAttack)
Log.attack("Directory Traversal", e);
throw e;
}
private static String getUntaintedPrefix(String s) {
@StringBuilder@ sb = new @StringBuilder@();
Taint t;
int len = s.length();
if (!s.@internal@isTainted()) return s;
t = s.@internal@taint();
for (int i = 0; i < len; i++) {
if (t.get(i))
break;
sb.append(s.charAt(i));
}
return sb.toString();
}
/** Determine if a file access is a directory traversal attack.
* A file access is allowed if:
* (a) The safeCanonicalPath of the untainted prefix is equal to the
* canonicalPath of the resulting filename, with trailing '/' from
* getCanonicalPath excluded.
*
* This occurs when the user inputs no additional path information, such
* as /untainted/foo/.. , where foo/.. is the untrusted suffix.
*
* (b) The canonical path of the resulting filename starts with
* the safeCanonicalpath of the untainted prefix.
* This is the actual directory traversal attack prevention.
*/
private static String doValidateFile(String untaintedPrefix, File result) {
String resultPath;
String prefixNoSlash = null;
int prefixLength = untaintedPrefix.length();
int rlen = fsRootPaths.length;
if (untaintedPrefix.endsWith(File.separator))
prefixNoSlash = untaintedPrefix.substring(0, prefixLength - 1);
try {
resultPath = result.getCanonicalPath();
} catch (Throwable e) {
Log.error(e);
return null;
}
for (int i = 0; i < rlen; i++)
if (fsRootPaths[i].equals(untaintedPrefix))
abortFile(untaintedPrefix, result,
"Untrusted input confined to root directory " +
fsRootPaths[i]);
if ((prefixNoSlash == null || !resultPath.equals(prefixNoSlash))
&& !resultPath.startsWith(untaintedPrefix))
abortFile(untaintedPrefix, result, "escaped from application " +
"directory");
return untaintedPrefix;
}
private static void scanNull(String path, File result) {
int len = path.length();
Taint t = path.@internal@taint();
for (int i = 0; i < len; i++)
if (path.charAt(i) == '\0') {
if (t != null && t.get(i))
abortFile(null, result, "Tainted null byte: " + path);
Log.warn("Untainted null byte in filename " + path);
}
}
/** Calculate the canonical path for an untainted prefix. We cannot
* just use File.getSafeCanonicalPath directly because we want the
* Canonical path for a <b> prefix </b> of a pathname, and this prefix
* may end in a directory name. The canonicalPath() of a directory name
* does not include the trailing file separator (/). This allows attacks
* like the following:
*
* Pathname: /usr/www/webroot/a/../admin-passwords.txt
* Untainted prefix: /usr/www/webroot/a/
* Tainted suffix: ../admin-passwords.txt
*
* getCanonicalPath(Untainted prefix) = /usr/www/webroot/a
* getCanonicalPath(Pathname) = /usr/www/webroot/admin-passwords.txt
*
* This would pass our security check requirements because
* getCanonicalPath(Pathname) starts with
* getCanonicalPath(Untainted Prefix). To prevent this attack,
* we use the getSafeCanonicalPath method. This method is a wrapper
* around getCanonicalPath, which appends an extra file separator to
* the output of getCanonicalPath if the original pathname ended in a
* file separator. So in the above example
*
* getSafeCanonicalPath(Untainted prefix) = /usr/www/webroot/a/
*
* and the above attack is correctly prevented.
*/
private static String getSafeCanonicalPath(String prefix, File prefixFile)
throws IOException
{
String prefixPath = prefixFile.getCanonicalPath();
if (prefix.length() > 0 && prefix.endsWith(File.separator))
if (!prefixPath.endsWith(File.separator))
prefixPath += File.separator;
return prefixPath;
}
public static String validateFile(File parent, String child, File result)
throws JTaintException
{
String p, c;
if (parent == null)
return validateFile(child, result);
try {
p = parent.@internal@untaintedPrefix();
/* A fully untainted file pathname has a 'null' untainted prefix */
if ((p == null && !child.@internal@isTainted())
|| !Configuration.filePolicyEnabled)
return null;
scanNull(child, result);
if ("".equals(parent.getPath()))
abortFile(parent.getCanonicalPath(), result,
"Untrusted input confined to root directory");
if (p != null)
return doValidateFile(getSafeCanonicalPath(p, new File(p)),
result);
c = getUntaintedPrefix(child);
if (c.length() != 0) {
return doValidateFile(
getSafeCanonicalPath(c, new File(parent, c)),
result);
} else {
/* see getSafeCanonicalPath() */
p = parent.getCanonicalPath();
if (!p.endsWith(File.separator))
p += File.separator;
return doValidateFile(getSafeCanonicalPath(p, new File(p)),
result);
}
} catch (JTaintException e) {
if (!e.isWhitelisted())
throw e;
return null;
} catch (Throwable e) {
Log.error(e);
return null;
}
}
public static String validateFile(String pathname, File result)
throws JTaintException
{
String u;
try {
if (!pathname.@internal@isTainted() ||
!Configuration.filePolicyEnabled)
return null;
scanNull(pathname, result);
if (!result.isAbsolute())
pathname = System.getProperty("user.dir") + File.separator +
pathname;
u = getUntaintedPrefix(pathname);
if (u.length() == 0)
abortFile(null, result, "Fully untrusted pathname");
return doValidateFile(getSafeCanonicalPath(u, new File(u)), result);
} catch (JTaintException e) {
if (!e.isWhitelisted())
throw e;
return null;
} catch (Throwable e) {
Log.error(e);
return null;
}
}
public static String validateFile(String parent, String child, File result)
throws JTaintException
{
String s, u;
try {
if (parent == null)
return validateFile(child, result);
if ((!parent.@internal@isTainted() && !child.@internal@isTainted())
|| !Configuration.filePolicyEnabled)
return null;
scanNull(parent, result);
scanNull(child, result);
if ("".equals(parent))
abortFile(null, result,
"Untrusted input confined to root directory");
if (!result.isAbsolute())
parent = System.getProperty("user.dir") + File.separator +
parent;
if (parent.@internal@isTainted()) {
u = getUntaintedPrefix(parent);
if (u.length() == 0)
abortFile(null, result, "Fully untrusted pathname");
return doValidateFile(getSafeCanonicalPath(u, new File(u)),
result);
} else {
u = getUntaintedPrefix(child);
if (u.length() != 0)
return doValidateFile(
getSafeCanonicalPath(u, new File(parent, u)),
result);
else {
/* See getSafeCanonicalPath */
if (!parent.endsWith("/"))
parent += "/";
return doValidateFile(
getSafeCanonicalPath(parent, new File(parent)),
result);
}
}
} catch (JTaintException e) {
if (!e.isWhitelisted())
throw e;
return null;
} catch (Throwable e) {
Log.error(e);
return null;
}
}
public static String validateFile(URI uri, File result)
throws JTaintException
{
String scheme, path, u;
URI tmp;
try {
if (!uri.isAbsolute() || uri.isOpaque())
return null; /* Invalid file URI */
scheme = uri.getScheme();
path = uri.getPath();
if ((!scheme.@internal@isTainted() && !path.@internal@isTainted())
|| !Configuration.filePolicyEnabled)
return null;
if (scheme.@internal@isTainted())
abortFile(null, result, "Untrusted filename URI scheme: " +
scheme);
scanNull(path, result);
if (!result.isAbsolute())
path = System.getProperty("user.dir") + File.separator + path;
/* Now check pathname as you would have previously */
u = getUntaintedPrefix(path);
if (u.length() == 0)
abortFile(null, result, "Fully untrusted pathname");
tmp = new URI(scheme, /* authority */ null,
/* path */ u, /* query */ null, /* fragment */ null);
return doValidateFile(getSafeCanonicalPath(u, new File(tmp)),
result);
} catch (JTaintException e) {
if (!e.isWhitelisted())
throw e;
return null;
} catch (Throwable e) {
Log.error(e);
return null;
}
}
}