├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src ├── main └── java │ └── org │ └── owasp │ └── fileio │ ├── Encoder.java │ ├── FileValidator.java │ ├── SafeFile.java │ ├── StringValidationRule.java │ ├── ValidationException.java │ ├── codecs │ ├── Codec.java │ ├── HTMLEntityCodec.java │ ├── HashTrie.java │ ├── PercentCodec.java │ ├── PushbackString.java │ └── Trie.java │ └── util │ ├── CollectionsUtil.java │ ├── NullSafe.java │ └── Utils.java └── test └── java └── org └── owasp └── fileio ├── FileValidatorTest.java ├── SafeFileTest.java └── util └── FileTestUtils.java /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject/private/ 2 | /build/ 3 | /dist/ 4 | /target/ 5 | 6 | .classpath 7 | .project 8 | .settings/ 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | dist: trusty 3 | jdk: 4 | - openjdk7 5 | - oraclejdk8 6 | - oraclejdk9 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/augustd/owasp-java-fileio.svg?branch=master)](https://travis-ci.org/augustd/owasp-java-fileio) 2 | 3 | The OWASP Java File I/O Security Project provides an easy to use library for validating and sanitizing filenames, directory paths, and uploaded files. This project encapsulates the file handling portions of the ESAPI project and makes them available in an easy to use library that has no dependencies. 4 | 5 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | org.owasp 5 | java-file-io 6 | 1.0.1-SNAPSHOT 7 | jar 8 | OWASP Java File IO 9 | The OWASP Java File I/O Security Project provides an easy to use library for validating and sanitizing filenames, directory paths, and uploaded files. 10 | https://github.com/augustd/owasp-java-fileio 11 | 12 | 13 | The Apache License, Version 2.0 14 | http://www.apache.org/licenses/LICENSE-2.0.txt 15 | 16 | 17 | 18 | 19 | August Detlefsen 20 | augustd@codemagi.com 21 | CodeMagi, Inc. 22 | http://www.codemagi.com 23 | 24 | 25 | 26 | scm:git:git@github.com:augustd/owasp-java-fileio.git 27 | scm:git:git@github.com:augustd/owasp-java-fileio.git 28 | https://github.com/augustd/owasp-java-fileio 29 | 30 | 31 | UTF-8 32 | 1.7 33 | 34 | 35 | 36 | 37 | 38 | org.apache.maven.plugins 39 | maven-clean-plugin 40 | 3.0.0 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-resources-plugin 45 | 3.0.2 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-compiler-plugin 50 | 3.7.0 51 | 52 | ${targetJdk} 53 | ${targetJdk} 54 | true 55 | true 56 | 57 | -Xlint 58 | 59 | 60 | 61 | 62 | org.apache.maven.plugins 63 | maven-jar-plugin 64 | 3.0.2 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-surefire-plugin 69 | 2.20.1 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-install-plugin 74 | 2.5.2 75 | 76 | 77 | 78 | 79 | 80 | 81 | release 82 | 83 | 84 | 85 | org.apache.maven.plugins 86 | maven-source-plugin 87 | 3.0.1 88 | 89 | 90 | attach-sources 91 | 92 | jar 93 | 94 | 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-javadoc-plugin 100 | 3.0.0-M1 101 | 102 | 103 | attach-javadocs 104 | 105 | jar 106 | 107 | 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-gpg-plugin 113 | 1.6 114 | 115 | 116 | sign-artifacts 117 | deploy 118 | 119 | sign 120 | 121 | 122 | 123 | 124 | 125 | org.sonatype.plugins 126 | nexus-staging-maven-plugin 127 | 1.6.7 128 | true 129 | 130 | ossrh 131 | https://oss.sonatype.org/ 132 | false 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | junit 142 | junit 143 | 4.12 144 | test 145 | 146 | 147 | 148 | 149 | ossrh 150 | https://oss.sonatype.org/content/repositories/snapshots 151 | 152 | 153 | ossrh 154 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/Encoder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio; 14 | 15 | import org.owasp.fileio.util.Utils; 16 | import java.util.ArrayList; 17 | import java.util.Iterator; 18 | import java.util.List; 19 | import java.util.Set; 20 | import org.owasp.fileio.codecs.Codec; 21 | import org.owasp.fileio.codecs.HTMLEntityCodec; 22 | import org.owasp.fileio.codecs.PercentCodec; 23 | 24 | /** 25 | * Reference implementation of the Encoder interface. This implementation takes a whitelist approach to encoding, meaning that everything not specifically identified in a list of "immune" characters 26 | * is encoded. 27 | */ 28 | public class Encoder { 29 | 30 | private static volatile Encoder singletonInstance; 31 | public final static char[] CHAR_ALPHANUMERICS = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 32 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 33 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; 34 | public static final Set ALPHANUMERICS; 35 | 36 | static { 37 | ALPHANUMERICS = Utils.arrayToSet(Encoder.CHAR_ALPHANUMERICS); 38 | } 39 | private boolean restrictMultiple = true; 40 | private boolean restrictMixed = true; 41 | 42 | public boolean isRestrictMultiple() { 43 | return restrictMultiple; 44 | } 45 | 46 | public void setRestrictMultiple(boolean restrictMultiple) { 47 | this.restrictMultiple = restrictMultiple; 48 | } 49 | 50 | public boolean isRestrictMixed() { 51 | return restrictMixed; 52 | } 53 | 54 | public void setRestrictMixed(boolean restrictMixed) { 55 | this.restrictMixed = restrictMixed; 56 | } 57 | 58 | public static Encoder getInstance() { 59 | if (singletonInstance == null) { 60 | synchronized (Encoder.class) { 61 | if (singletonInstance 62 | == null) { 63 | singletonInstance = new Encoder(); 64 | } 65 | } 66 | } 67 | return singletonInstance; 68 | } 69 | // Codecs 70 | private List codecs = new ArrayList<>(); 71 | private HTMLEntityCodec htmlCodec = new HTMLEntityCodec(); 72 | private PercentCodec percentCodec = new PercentCodec(); 73 | 74 | /** 75 | * Instantiates a new DefaultEncoder with the default codecs 76 | */ 77 | public Encoder() { 78 | codecs.add(htmlCodec); 79 | codecs.add(percentCodec); 80 | } 81 | 82 | /** 83 | * Instantiates a new DefaultEncoder with the default codecs 84 | * @param codecs A List of Codecs to use 85 | */ 86 | public Encoder(List codecs) { 87 | this.codecs = codecs; 88 | } 89 | 90 | /** 91 | * {@inheritDoc} 92 | */ 93 | public String canonicalize(String input) { 94 | if (input == null) { 95 | return null; 96 | } 97 | 98 | // Issue 231 - These are reverse boolean logic in the Encoder interface, so we need to invert these values - CS 99 | return canonicalize(input, restrictMultiple, restrictMixed); 100 | } 101 | 102 | /** 103 | * {@inheritDoc} 104 | */ 105 | public String canonicalize(String input, boolean strict) { 106 | return canonicalize(input, strict, strict); 107 | } 108 | 109 | /** 110 | * {@inheritDoc} 111 | */ 112 | public String canonicalize(String input, boolean restrictMultiple, boolean restrictMixed) { 113 | if (input == null) { 114 | return null; 115 | } 116 | 117 | String working = input; 118 | Codec codecFound = null; 119 | int mixedCount = 1; 120 | int foundCount = 0; 121 | boolean clean = false; 122 | while (!clean) { 123 | clean = true; 124 | 125 | // try each codec and keep track of which ones work 126 | Iterator i = codecs.iterator(); 127 | while (i.hasNext()) { 128 | Codec codec = i.next(); 129 | String old = working; 130 | working = codec.decode(working); 131 | if (!old.equals(working)) { 132 | if (codecFound != null && codecFound != codec) { 133 | mixedCount++; 134 | } 135 | codecFound = codec; 136 | if (clean) { 137 | foundCount++; 138 | } 139 | clean = false; 140 | } 141 | } 142 | } 143 | 144 | // do strict tests and handle if any mixed, multiple, nested encoding were found 145 | if (foundCount >= 2 && mixedCount > 1) { 146 | if (restrictMultiple || restrictMixed) { 147 | //TODO: throw new ValidationException("Input validation failure", "Multiple (" + foundCount + "x) and mixed encoding (" + mixedCount + "x) detected in " + input); 148 | } else { 149 | //TODO: logger.warning(Logger.SECURITY_FAILURE, "Multiple (" + foundCount + "x) and mixed encoding (" + mixedCount + "x) detected in " + input); 150 | } 151 | } else if (foundCount >= 2) { 152 | if (restrictMultiple) { 153 | //TODO: throw new ValidationException("Input validation failure", "Multiple (" + foundCount + "x) encoding detected in " + input); 154 | } else { 155 | //TODO: logger.warning(Logger.SECURITY_FAILURE, "Multiple (" + foundCount + "x) encoding detected in " + input); 156 | } 157 | } else if (mixedCount > 1) { 158 | if (restrictMixed) { 159 | //TODO: throw new ValidationException("Input validation failure", "Mixed encoding (" + mixedCount + "x) detected in " + input); 160 | } else { 161 | //TODO: logger.warning(Logger.SECURITY_FAILURE, "Mixed encoding (" + mixedCount + "x) detected in " + input); 162 | } 163 | } 164 | return working; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/FileValidator.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author Jim Manico (jim@manico.net) Manico.net - Original ESAPI author 11 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 12 | * @created 2014 13 | */ 14 | package org.owasp.fileio; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.Arrays; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | import java.util.regex.Pattern; 23 | import org.owasp.fileio.util.Utils; 24 | 25 | /** 26 | * Reference implementation of the FileValidator. This implementation 27 | * provides basic validation functions. This library 28 | * has a heavy emphasis on whitelist validation and canonicalization. 29 | * 30 | * @author August Detlefsen CodeMagi 31 | */ 32 | public class FileValidator { 33 | 34 | // Validation of file related input 35 | public static final String FILE_NAME_REGEX = "^[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$"; 36 | public static final String DIRECTORY_NAME_REGEX = "^[a-zA-Z0-9:/\\\\!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$"; 37 | 38 | /** 39 | * The encoder to use for file system 40 | */ 41 | private Encoder fileEncoder; 42 | 43 | /** 44 | * The maximum allowable upload size 45 | */ 46 | private Long maxFileUploadSize = 500000000l; 47 | 48 | /** 49 | * The maximum allowable file path length 50 | */ 51 | private Integer maxFilePathSize = 255; 52 | 53 | /** 54 | * The file extension that will be allowed by this validator 55 | */ 56 | List allowedExtensions = new ArrayList(); 57 | 58 | /** 59 | * Initialize file validator with an appropriate set of codecs 60 | */ 61 | public FileValidator() { 62 | fileEncoder = new Encoder(); 63 | } 64 | 65 | /** 66 | * Initialize file validator with an appropriate set of codecs 67 | * 68 | * @param encoder The encoder instance to use 69 | */ 70 | public FileValidator(Encoder encoder) { 71 | fileEncoder = encoder; 72 | } 73 | 74 | //GETTERS AND SETTERS -------------------------------------------------------------- 75 | 76 | public Long getMaxFileUploadSize() { 77 | return maxFileUploadSize; 78 | } 79 | 80 | public void setMaxFileUploadSize(Long maxFileUploadSize) { 81 | this.maxFileUploadSize = maxFileUploadSize; 82 | } 83 | 84 | public List getAllowedExtensions() { 85 | return allowedExtensions; 86 | } 87 | 88 | public void setAllowedExtensions(List allowedExtensions) { 89 | this.allowedExtensions = allowedExtensions; 90 | } 91 | 92 | public Integer getMaxFilePathSize() { 93 | return maxFilePathSize; 94 | } 95 | 96 | public void setMaxFilePathSize(Integer maxFilePathSize) { 97 | this.maxFilePathSize = maxFilePathSize; 98 | } 99 | 100 | public Encoder getFileEncoder() { 101 | return fileEncoder; 102 | } 103 | 104 | public void setFileEncoder(Encoder fileEncoder) { 105 | this.fileEncoder = fileEncoder; 106 | } 107 | 108 | 109 | 110 | /** 111 | * Calls getValidDirectoryPath and returns true if no exceptions are thrown. 112 | * 113 | *

Note: On platforms that support symlinks, this function will fail canonicalization if directorypath is a symlink. For example, on MacOS X, /etc is actually /private/etc. If you mean 114 | * to use /etc, use its real path (/private/etc), not the symlink (/etc).

115 | * 116 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 117 | * value passed in. 118 | * @param input The actual input data to validate. 119 | * @param parent A File indicating the parent directory into which the input File will be placed. 120 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 121 | * 122 | * @return true if no validation exceptions are thrown 123 | */ 124 | public boolean isValidDirectoryPath(String context, String input, File parent, boolean allowNull) { 125 | try { 126 | getValidDirectoryPath(context, input, parent, allowNull); 127 | return true; 128 | } catch (ValidationException e) { 129 | return false; 130 | } 131 | } 132 | 133 | /** 134 | * Calls getValidDirectoryPath and returns true if no exceptions are thrown. 135 | * 136 | *

Note: On platforms that support symlinks, this function will fail canonicalization if directorypath is a symlink. For example, on MacOS X, /etc is actually /private/etc. If you mean 137 | * to use /etc, use its real path (/private/etc), not the symlink (/etc).

138 | * 139 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 140 | * value passed in. 141 | * @param input The actual input data to validate. 142 | * @param parent A File indicating the parent directory into which the input File will be placed. 143 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 144 | * @param errors A List to contain any validation errors. 145 | * 146 | * @return true if no validation exceptions are thrown 147 | */ 148 | public boolean isValidDirectoryPath(String context, String input, File parent, boolean allowNull, List errors) { 149 | try { 150 | getValidDirectoryPath(context, input, parent, allowNull); 151 | return true; 152 | } catch (ValidationException e) { 153 | errors.add(e); 154 | } 155 | 156 | return false; 157 | } 158 | 159 | /** 160 | * Returns a canonicalized and validated directory path as a String, provided that the input maps to an existing directory that is an existing subdirectory (at any level) of the specified parent. 161 | * Invalid input will generate a descriptive ValidationException, and input that is clearly an attack will generate a descriptive IntrusionException. Instead of throwing a ValidationException on 162 | * error, this variant will store the exception inside of the ValidationErrorList. 163 | * 164 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 165 | * value passed in. 166 | * @param input The actual input data to validate. 167 | * @param parent A File indicating the parent directory into which the input File will be placed. 168 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 169 | * 170 | * @return A valid directory path 171 | * 172 | * @throws ValidationException if validation errors occur 173 | */ 174 | public String getValidDirectoryPath(String context, String input, File parent, boolean allowNull) throws ValidationException { 175 | try { 176 | if (Utils.isEmpty(input)) { 177 | if (allowNull) { 178 | return null; 179 | } 180 | throw new ValidationException(context + ": Input directory path required", "Input directory path required: context=" + context + ", input=" + input, context); 181 | } 182 | 183 | File dir = new File(input); 184 | 185 | // check dir exists and parent exists and dir is inside parent 186 | if (!dir.exists()) { 187 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory, does not exist: context=" + context + ", input=" + input); 188 | } 189 | if (!dir.isDirectory()) { 190 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory, not a directory: context=" + context + ", input=" + input); 191 | } 192 | if (!parent.exists()) { 193 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory, specified parent does not exist: context=" + context + ", input=" + input + ", parent=" + parent); 194 | } 195 | if (!parent.isDirectory()) { 196 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory, specified parent is not a directory: context=" + context + ", input=" + input + ", parent=" + parent); 197 | } 198 | if (!dir.getCanonicalPath().startsWith(parent.getCanonicalPath())) { 199 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory, not inside specified parent: context=" + context + ", input=" + input + ", parent=" + parent); 200 | } 201 | 202 | // check canonical form matches input 203 | String canonicalPath = dir.getCanonicalPath(); 204 | String canonical = getValidInput(context, canonicalPath, DIRECTORY_NAME_REGEX, maxFilePathSize, false); 205 | if (!canonical.equals(input)) { 206 | throw new ValidationException(context + ": Invalid directory name", "Invalid directory name does not match the canonical path: context=" + context + ", input=" + input + ", canonical=" + canonical, context); 207 | } 208 | return canonical; 209 | } catch (Exception e) { 210 | throw new ValidationException(context + ": Invalid directory name", "Failure to validate directory path: context=" + context + ", input=" + input, e, context); 211 | } 212 | } 213 | 214 | /** 215 | * Calls getValidDirectoryPath with the supplied error List to capture ValidationExceptions 216 | * 217 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 218 | * value passed in. 219 | * @param input The actual input data to validate. 220 | * @param parent A File indicating the parent directory into which the input File will be placed. 221 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 222 | * @param errors A List to contain any validation errors. 223 | * 224 | * @return A valid directory path 225 | */ 226 | public String getValidDirectoryPath(String context, String input, File parent, boolean allowNull, List errors) { 227 | 228 | try { 229 | return getValidDirectoryPath(context, input, parent, allowNull); 230 | } catch (ValidationException e) { 231 | errors.add(e); 232 | } 233 | 234 | return ""; 235 | } 236 | 237 | /** 238 | * Calls getValidFileName with the default list of allowedExtensions 239 | * 240 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 241 | * value passed in. 242 | * @param input The actual input data to validate. 243 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 244 | * 245 | * @return true if no validation exceptions occur 246 | */ 247 | public boolean isValidFileName(String context, String input, boolean allowNull) { 248 | return isValidFileName(context, input, null, allowNull); 249 | } 250 | 251 | /** 252 | * Calls getValidFileName with the default list of allowedExtensions 253 | * 254 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 255 | * value passed in. 256 | * @param input The actual input data to validate. 257 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 258 | * @param errors A List to contain any validation errors. 259 | * 260 | * @return true if no validation exceptions occur 261 | */ 262 | public boolean isValidFileName(String context, String input, boolean allowNull, List errors) { 263 | return isValidFileName(context, input, null, allowNull, errors); 264 | } 265 | 266 | /** 267 | * Calls getValidFileName with the default list of allowedExtensions 268 | * 269 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 270 | * value passed in. 271 | * @param input The actual input data to validate. 272 | * @param allowedExtensions A List of allowed file extensions to validate against 273 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 274 | * 275 | * @return true if no validation exceptions occur 276 | */ 277 | public boolean isValidFileName(String context, String input, List allowedExtensions, boolean allowNull) { 278 | try { 279 | getValidFileName(context, input, allowedExtensions, allowNull); 280 | return true; 281 | } catch (Exception e) { 282 | return false; 283 | } 284 | } 285 | 286 | /** 287 | * Calls getValidFileName with the default list of allowedExtensions 288 | * 289 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 290 | * value passed in. 291 | * @param input The actual input data to validate. 292 | * @param allowedExtensions A List of allowed file extensions to validate against 293 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 294 | * @param errors A List to contain any validation errors. 295 | * 296 | * @return true if no validation exceptions occur 297 | */ 298 | public boolean isValidFileName(String context, String input, List allowedExtensions, boolean allowNull, List errors) { 299 | try { 300 | getValidFileName(context, input, allowedExtensions, allowNull); 301 | return true; 302 | } catch (ValidationException e) { 303 | errors.add(e); 304 | } 305 | 306 | return false; 307 | } 308 | 309 | /** 310 | * Returns a canonicalized and validated file name as a String. Implementors should check for allowed file extensions here, as well as allowed file name characters, as declared in 311 | * "ESAPI.properties". Invalid input will generate a descriptive ValidationException, and input that is clearly an attack will generate a descriptive IntrusionException. 312 | * 313 | * Note: If you do not explicitly specify a white list of allowed extensions, all extensions will be allowed by default. 314 | * 315 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 316 | * value passed in. 317 | * @param input The actual input data to validate. 318 | * @param allowedExtensions A List of allowed file extensions to validate against 319 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 320 | * 321 | * @return A valid file name 322 | * 323 | * @throws ValidationException if validation errors occur 324 | */ 325 | public String getValidFileName(String context, String input, List allowedExtensions, boolean allowNull) throws ValidationException { 326 | 327 | String canonical = ""; 328 | // detect path manipulation 329 | try { 330 | if (Utils.isEmpty(input)) { 331 | if (allowNull) { 332 | return null; 333 | } 334 | throw new ValidationException(context + ": Input file name required", "Input required: context=" + context + ", input=" + input, context); 335 | } 336 | 337 | // do basic validation 338 | canonical = new File(input).getCanonicalFile().getName(); 339 | getValidInput(context, input, FILE_NAME_REGEX, 255, true); 340 | 341 | File f = new File(canonical); 342 | String c = f.getCanonicalPath(); 343 | String cpath = c.substring(c.lastIndexOf(File.separator) + 1); 344 | 345 | 346 | // the path is valid if the input matches the canonical path 347 | if (!input.equals(cpath)) { 348 | throw new ValidationException(context + ": Invalid file name", "Invalid directory name does not match the canonical path: context=" + context + ", input=" + input + ", canonical=" + canonical, context); 349 | } 350 | 351 | } catch (IOException e) { 352 | throw new ValidationException(context + ": Invalid file name", "Invalid file name does not exist: context=" + context + ", canonical=" + canonical, e, context); 353 | } 354 | 355 | // verify extensions 356 | if ((allowedExtensions == null) || (allowedExtensions.isEmpty())) { 357 | return canonical; 358 | } else { 359 | Iterator i = allowedExtensions.iterator(); 360 | while (i.hasNext()) { 361 | String ext = i.next(); 362 | if (input.toLowerCase().endsWith(ext.toLowerCase())) { 363 | return canonical; 364 | } 365 | } 366 | throw new ValidationException(context + ": Invalid file name does not have valid extension ( " + allowedExtensions + ")", "Invalid file name does not have valid extension ( " + allowedExtensions + "): context=" + context + ", input=" + input, context); 367 | } 368 | } 369 | 370 | /** 371 | * Calls getValidFileName with the supplied List to capture ValidationExceptions 372 | * 373 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 374 | * value passed in. 375 | * @param input The actual input data to validate. 376 | * @param allowedExtensions A List of allowed file extensions to validate against 377 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 378 | * @param errors A List to contain any validation errors. 379 | * 380 | * @return A valid file name 381 | */ 382 | public String getValidFileName(String context, String input, List allowedExtensions, boolean allowNull, List errors) { 383 | try { 384 | return getValidFileName(context, input, allowedExtensions, allowNull); 385 | } catch (ValidationException e) { 386 | errors.add(e); 387 | } 388 | 389 | return ""; 390 | } 391 | 392 | /** 393 | * Calls getValidFileUpload and returns true if no exceptions are thrown. 394 | * 395 | *

Note: On platforms that support symlinks, this function will fail canonicalization if directorypath is a symlink. For example, on MacOS X, /etc is actually /private/etc. If you mean 396 | * to use /etc, use its real path (/private/etc), not the symlink (/etc).

397 | * 398 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 399 | * value passed in. 400 | * @param directorypath The file path of the uploaded file. 401 | * @param filename The filename of the uploaded file 402 | * @param parent A File indicating the parent directory into which the input File will be placed. 403 | * @param content A byte array containing the content of the uploaded file. 404 | * @param maxBytes The max number of bytes allowed for a legal file upload. 405 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 406 | * 407 | * @return true if no validation exceptions are thrown 408 | * 409 | * @throws ValidationException if validation errors occur 410 | */ 411 | public boolean isValidFileUpload(String context, String directorypath, String filename, File parent, byte[] content, int maxBytes, boolean allowNull) throws ValidationException { 412 | return (isValidFileName(context, filename, allowNull) 413 | && isValidDirectoryPath(context, directorypath, parent, allowNull) 414 | && isValidFileContent(context, content, maxBytes, allowNull)); 415 | } 416 | 417 | /** 418 | * Calls getValidFileUpload and returns true if no exceptions are thrown. 419 | * 420 | *

Note: On platforms that support symlinks, this function will fail canonicalization if directorypath is a symlink. For example, on MacOS X, /etc is actually /private/etc. If you mean 421 | * to use /etc, use its real path (/private/etc), not the symlink (/etc).

422 | * 423 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 424 | * value passed in. 425 | * @param directorypath The file path of the uploaded file. 426 | * @param filename The filename of the uploaded file 427 | * @param parent A File indicating the parent directory into which the input File will be placed. 428 | * @param content A byte array containing the content of the uploaded file. 429 | * @param maxBytes The max number of bytes allowed for a legal file upload. 430 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 431 | * @param errors A List to contain any validation errors. 432 | * 433 | * @return true if no validation exceptions are thrown 434 | */ 435 | public boolean isValidFileUpload(String context, String directorypath, String filename, File parent, byte[] content, int maxBytes, boolean allowNull, List errors) { 436 | return (isValidFileName(context, filename, allowNull, errors) 437 | && isValidDirectoryPath(context, directorypath, parent, allowNull, errors) 438 | && isValidFileContent(context, content, maxBytes, allowNull, errors)); 439 | } 440 | 441 | /** 442 | * Validates the filepath, filename, and content of a file. Invalid input will generate a descriptive ValidationException. 443 | * 444 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 445 | * value passed in. 446 | * @param directorypath The file path of the uploaded file. 447 | * @param filename The filename of the uploaded file 448 | * @param parent A File indicating the parent directory into which the input File will be placed. 449 | * @param content A byte array containing the content of the uploaded file. 450 | * @param maxBytes The max number of bytes allowed for a legal file upload. 451 | * @param allowedExtensions A List of allowed file extensions to validate against 452 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 453 | * 454 | * @throws ValidationException if validation errors occur 455 | */ 456 | public void assertValidFileUpload(String context, String directorypath, String filename, File parent, byte[] content, int maxBytes, List allowedExtensions, boolean allowNull) throws ValidationException { 457 | getValidFileName(context, filename, allowedExtensions, allowNull); 458 | getValidDirectoryPath(context, directorypath, parent, allowNull); 459 | getValidFileContent(context, content, maxBytes, allowNull); 460 | } 461 | 462 | /** 463 | * Calls getValidFileUpload with the supplied List to capture ValidationExceptions 464 | * 465 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 466 | * value passed in. 467 | * @param directorypath The file path of the uploaded file. 468 | * @param filename The filename of the uploaded file 469 | * @param parent A File indicating the parent directory into which the input File will be placed. 470 | * @param content A byte array containing the content of the uploaded file. 471 | * @param maxBytes The max number of bytes allowed for a legal file upload. 472 | * @param allowedExtensions A List of allowed file extensions to validate against 473 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 474 | * @param errors A List to contain any validation errors. 475 | */ 476 | public void assertValidFileUpload(String context, String directorypath, String filename, File parent, byte[] content, int maxBytes, List allowedExtensions, boolean allowNull, List errors) { 477 | try { 478 | assertValidFileUpload(context, directorypath, filename, parent, content, maxBytes, allowedExtensions, allowNull); 479 | } catch (ValidationException e) { 480 | errors.add(e); 481 | } 482 | } 483 | 484 | /** 485 | * Calls getValidFileContent and returns true if no exceptions are thrown. 486 | * 487 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 488 | * value passed in. 489 | * @param input The actual input data to validate. 490 | * @param maxBytes The max number of bytes allowed for a legal file upload. 491 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 492 | * 493 | * @return true if no validation exceptions occur 494 | */ 495 | public boolean isValidFileContent(String context, byte[] input, int maxBytes, boolean allowNull) { 496 | try { 497 | getValidFileContent(context, input, maxBytes, allowNull); 498 | return true; 499 | } catch (Exception e) { 500 | return false; 501 | } 502 | } 503 | 504 | /** 505 | * Calls getValidFileContent and returns true if no exceptions are thrown. 506 | * 507 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 508 | * value passed in. 509 | * @param input The actual input data to validate. 510 | * @param maxBytes The max number of bytes allowed for a legal file upload. 511 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 512 | * @param errors A List to contain any validation errors. 513 | * 514 | * @return true if no validation exceptions occur 515 | */ 516 | public boolean isValidFileContent(String context, byte[] input, int maxBytes, boolean allowNull, List errors) { 517 | try { 518 | getValidFileContent(context, input, maxBytes, allowNull); 519 | return true; 520 | } catch (ValidationException e) { 521 | errors.add(e); 522 | return false; 523 | } 524 | } 525 | 526 | /** 527 | * Returns validated file content as a byte array. This method checks for max file size (according to the value configured in the maxFileUploadSize class variable) 528 | * and null input ONLY. It can be extended to check for allowed character sets, and do virus scans. Invalid 529 | * input will generate a descriptive ValidationException. 530 | * 531 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 532 | * value passed in. 533 | * @param input The actual input data to validate. 534 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 535 | * 536 | * @return A byte array containing valid file content. 537 | * 538 | * @throws ValidationException if validation errors occur 539 | */ 540 | public byte[] getValidFileContent(String context, byte[] input, boolean allowNull) throws ValidationException { 541 | return getValidFileContent(context, input, getMaxFileUploadSize(), allowNull); 542 | } 543 | 544 | /** 545 | * Returns validated file content as a byte array. This method checks for max file size and null input ONLY. It can be extended to check for allowed character sets, and do virus scans. Invalid 546 | * input will generate a descriptive ValidationException. 547 | * 548 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 549 | * value passed in. 550 | * @param input The actual input data to validate. 551 | * @param maxBytes The max number of bytes allowed for a legal file upload. 552 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 553 | * 554 | * @return A byte array containing valid file content. 555 | * 556 | * @throws ValidationException if validation errors occur 557 | */ 558 | public byte[] getValidFileContent(String context, byte[] input, long maxBytes, boolean allowNull) throws ValidationException { 559 | if (Utils.isEmpty(input)) { 560 | if (allowNull) { 561 | return null; 562 | } 563 | throw new ValidationException(context + ": Input required", "Input required: context=" + context + ", input=" + Arrays.toString(input), context); 564 | } 565 | 566 | if (input.length > maxBytes) { 567 | throw new ValidationException(context + ": Invalid file content can not exceed " + maxBytes + " bytes", "Exceeded maxBytes ( " + input.length + ")", context); 568 | } 569 | 570 | return input; 571 | } 572 | 573 | /** 574 | * Calls getValidFileContent with the supplied List to capture ValidationExceptions 575 | * 576 | * @param context A descriptive name of the parameter that you are validating (e.g., LoginPage_UsernameField). This value is used by any logging or error handling that is done with respect to the 577 | * value passed in. 578 | * @param input The actual input data to validate. 579 | * @param maxBytes The max number of bytes allowed for a legal file upload. 580 | * @param allowNull If allowNull is true then an input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 581 | * @param errors A List to contain any validation errors. 582 | * 583 | * @return A byte array containing valid file content. 584 | * 585 | * @throws ValidationException if validation errors occur 586 | */ 587 | public byte[] getValidFileContent(String context, byte[] input, int maxBytes, boolean allowNull, List errors) throws ValidationException { 588 | try { 589 | return getValidFileContent(context, input, maxBytes, allowNull); 590 | } catch (ValidationException e) { 591 | errors.add(e); 592 | } 593 | // return empty byte array on error 594 | return new byte[0]; 595 | } 596 | 597 | /** 598 | * Validates data received from the browser and returns a safe version. Double encoding is treated as an attack. The default encoder supports html encoding, URL encoding, and javascript escaping. 599 | * Input is canonicalized by default before validation. 600 | * 601 | * @param context A descriptive name for the field to validate. This is used for error facing validation messages and element identification. 602 | * @param input The actual user input data to validate. 603 | * @param type The regular expression name which maps to the actual regular expression from "ESAPI.properties". 604 | * @param maxLength The maximum post-canonicalized String length allowed. 605 | * @param allowNull If allowNull is true then a input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 606 | * 607 | * @return The canonicalized user input. 608 | * 609 | * @throws ValidationException if validation errors occur 610 | */ 611 | public String getValidInput(String context, String input, String type, int maxLength, boolean allowNull) throws ValidationException { 612 | return getValidInput(context, input, type, maxLength, allowNull, true); 613 | } 614 | 615 | /** 616 | * Validates data received from the browser and returns a safe version. Only URL encoding is supported. Double encoding is treated as an attack. 617 | * 618 | * @param context A descriptive name for the field to validate. This is used for error facing validation messages and element identification. 619 | * @param input The actual user input data to validate. 620 | * @param type The regular expression name which maps to the actual regular expression in the ESAPI validation configuration file 621 | * @param maxLength The maximum String length allowed. If input is canonicalized per the canonicalize argument, then maxLength must be verified after canonicalization 622 | * @param allowNull If allowNull is true then a input that is NULL or an empty string will be legal. If allowNull is false then NULL or an empty String will throw a ValidationException. 623 | * @param canonicalize If canonicalize is true then input will be canonicalized before validation 624 | * 625 | * @return The user input, may be canonicalized if canonicalize argument is true 626 | * 627 | * @throws ValidationException if validation errors occur 628 | */ 629 | public String getValidInput(String context, String input, String type, int maxLength, boolean allowNull, boolean canonicalize) throws ValidationException { 630 | StringValidationRule rvr = new StringValidationRule(type, fileEncoder); 631 | 632 | Pattern p = Pattern.compile(type); 633 | rvr.addWhitelistPattern( p ); 634 | 635 | rvr.setMaximumLength(maxLength); 636 | rvr.setAllowNull(allowNull); 637 | rvr.setValidateInputAndCanonical(canonicalize); 638 | return rvr.getValid(context, input); 639 | } 640 | 641 | } -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/SafeFile.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Arshan Dabirsiaghi Aspect Security 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio; 14 | 15 | import java.io.File; 16 | import java.net.URI; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | 20 | /** 21 | * Extension to java.io.File to prevent against null byte injections and other unforeseen problems resulting from unprintable characters causing problems in path lookups. This does _not_ prevent 22 | * against directory traversal attacks. 23 | */ 24 | public class SafeFile extends File { 25 | 26 | private static final long serialVersionUID = 1L; 27 | private static final Pattern PERCENTS_PAT = Pattern.compile("(%)([0-9a-fA-F])([0-9a-fA-F])"); 28 | private static final Pattern FILE_BLACKLIST_PAT = Pattern.compile("([\\\\/:*?<>|])"); 29 | private static final Pattern DIR_BLACKLIST_PAT = Pattern.compile("([*?<>|])"); 30 | 31 | public SafeFile(String path) throws ValidationException { 32 | super(path); 33 | doDirCheck(this.getParent()); 34 | doFileCheck(this.getName()); 35 | } 36 | 37 | public SafeFile(String parent, String child) throws ValidationException { 38 | super(parent, child); 39 | doDirCheck(this.getParent()); 40 | doFileCheck(this.getName()); 41 | } 42 | 43 | public SafeFile(File parent, String child) throws ValidationException { 44 | super(parent, child); 45 | doDirCheck(this.getParent()); 46 | doFileCheck(this.getName()); 47 | } 48 | 49 | public SafeFile(URI uri) throws ValidationException { 50 | super(uri); 51 | doDirCheck(this.getParent()); 52 | doFileCheck(this.getName()); 53 | } 54 | 55 | private void doDirCheck(String path) throws ValidationException { 56 | if (path == null) return; 57 | Matcher m1 = DIR_BLACKLIST_PAT.matcher(path); 58 | if (m1.find()) { 59 | throw new ValidationException("Invalid directory", "Directory path (" + path + ") contains illegal character: " + m1.group()); 60 | } 61 | 62 | Matcher m2 = PERCENTS_PAT.matcher(path); 63 | if (m2.find()) { 64 | throw new ValidationException("Invalid directory", "Directory path (" + path + ") contains encoded characters: " + m2.group()); 65 | } 66 | 67 | int ch = containsUnprintableCharacters(path); 68 | if (ch != -1) { 69 | throw new ValidationException("Invalid directory", "Directory path (" + path + ") contains unprintable character: " + ch); 70 | } 71 | } 72 | 73 | private void doFileCheck(String path) throws ValidationException { 74 | Matcher m1 = FILE_BLACKLIST_PAT.matcher(path); 75 | if (m1.find()) { 76 | throw new ValidationException("Invalid directory", "Directory path (" + path + ") contains illegal character: " + m1.group()); 77 | } 78 | 79 | Matcher m2 = PERCENTS_PAT.matcher(path); 80 | if (m2.find()) { 81 | throw new ValidationException("Invalid file", "File path (" + path + ") contains encoded characters: " + m2.group()); 82 | } 83 | 84 | int ch = containsUnprintableCharacters(path); 85 | if (ch != -1) { 86 | throw new ValidationException("Invalid file", "File path (" + path + ") contains unprintable character: " + ch); 87 | } 88 | } 89 | 90 | private int containsUnprintableCharacters(String s) { 91 | for (int i = 0; i < s.length(); i++) { 92 | char ch = s.charAt(i); 93 | if (((int) ch) < 32 || ((int) ch) > 126) { 94 | return (int) ch; 95 | } 96 | } 97 | return -1; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/StringValidationRule.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Set; 18 | import java.util.regex.Pattern; 19 | import java.util.regex.PatternSyntaxException; 20 | import org.owasp.fileio.util.NullSafe; 21 | import org.owasp.fileio.util.Utils; 22 | 23 | /** 24 | * A validator performs syntax and possibly semantic validation of a single piece of data from an untrusted source. 25 | */ 26 | public class StringValidationRule { 27 | 28 | protected String typeName; 29 | protected Encoder encoder; 30 | protected boolean allowNull = false; 31 | protected List whitelistPatterns = new ArrayList(); 32 | protected List blacklistPatterns = new ArrayList(); 33 | protected int minLength = 0; 34 | protected int maxLength = Integer.MAX_VALUE; 35 | protected boolean validateInputAndCanonical = true; 36 | 37 | public StringValidationRule(String typeName) { 38 | this.typeName = typeName; 39 | } 40 | 41 | public StringValidationRule(String typeName, Encoder encoder) { 42 | this.typeName = typeName; 43 | this.encoder = encoder; 44 | } 45 | 46 | public StringValidationRule(String typeName, Encoder encoder, String whitelistPattern) { 47 | this.typeName = typeName; 48 | this.encoder = encoder; 49 | addWhitelistPattern(whitelistPattern); 50 | } 51 | 52 | /** 53 | * @param pattern A String which will be compiled into a regular expression pattern to add to the whitelist 54 | * @throws IllegalArgumentException if pattern is null 55 | */ 56 | public void addWhitelistPattern(String pattern) { 57 | if (pattern == null) { 58 | throw new IllegalArgumentException("Pattern cannot be null"); 59 | } 60 | try { 61 | whitelistPatterns.add(Pattern.compile(pattern)); 62 | } catch (PatternSyntaxException e) { 63 | throw new IllegalArgumentException("Validation misconfiguration, problem with specified pattern: " + pattern, e); 64 | } 65 | } 66 | 67 | /** 68 | * @param p A regular expression pattern to add to the whitelist 69 | * @throws IllegalArgumentException if p is null 70 | */ 71 | public void addWhitelistPattern(Pattern p) { 72 | if (p == null) { 73 | throw new IllegalArgumentException("Pattern cannot be null"); 74 | } 75 | whitelistPatterns.add(p); 76 | } 77 | 78 | /** 79 | * @param pattern A String which will be compiled into a regular expression pattern to add to the blacklist 80 | 81 | * @throws IllegalArgumentException if pattern is null 82 | */ 83 | public void addBlacklistPattern(String pattern) { 84 | if (pattern == null) { 85 | throw new IllegalArgumentException("Pattern cannot be null"); 86 | } 87 | try { 88 | blacklistPatterns.add(Pattern.compile(pattern)); 89 | } catch (PatternSyntaxException e) { 90 | throw new IllegalArgumentException("Validation misconfiguration, problem with specified pattern: " + pattern, e); 91 | } 92 | } 93 | 94 | /** 95 | * @param p A regular expression pattern to add to the blacklist 96 | * @throws IllegalArgumentException if p is null 97 | */ 98 | public void addBlacklistPattern(Pattern p) { 99 | if (p == null) { 100 | throw new IllegalArgumentException("Pattern cannot be null"); 101 | } 102 | blacklistPatterns.add(p); 103 | } 104 | 105 | public void setMinimumLength(int length) { 106 | minLength = length; 107 | } 108 | 109 | public void setMaximumLength(int length) { 110 | maxLength = length; 111 | } 112 | 113 | /** 114 | * Set the flag which determines whether the in input itself is checked as well as the canonical form of the input. 115 | * 116 | * @param flag The value to set 117 | */ 118 | public void setValidateInputAndCanonical(boolean flag) { 119 | validateInputAndCanonical = flag; 120 | } 121 | 122 | /** 123 | * checks input against whitelists. 124 | * 125 | * @param context The context to include in exception messages 126 | * @param input the input to check 127 | * @param orig A original input to include in exception messages. This is not included if it is the same as input. 128 | * @return input upon a successful check 129 | * @throws ValidationException if the check fails. 130 | */ 131 | private String checkWhitelist(String context, String input, String orig) throws ValidationException { 132 | // check whitelist patterns 133 | for (Pattern p : whitelistPatterns) { 134 | if (!p.matcher(input).matches()) { 135 | throw new ValidationException(context + ": Invalid input. Please conform to regex " + p.pattern() + (maxLength == Integer.MAX_VALUE ? "" : " with a maximum length of " + maxLength), "Invalid input: context=" + context + ", type(" + getTypeName() + ")=" + p.pattern() + ", input=" + input + (NullSafe.equals(orig, input) ? "" : ", orig=" + orig), context); 136 | } 137 | } 138 | 139 | return input; 140 | } 141 | 142 | /** 143 | * checks input against whitelists. 144 | * 145 | * @param context The context to include in exception messages 146 | * @param input the input to check 147 | * @return input upon a successful check 148 | * @throws ValidationException if the check fails. 149 | */ 150 | private String checkWhitelist(String context, String input) throws ValidationException { 151 | return checkWhitelist(context, input, input); 152 | } 153 | 154 | /** 155 | * checks input against blacklists. 156 | * 157 | * @param context The context to include in exception messages 158 | * @param input the input to check 159 | * @param orig A original input to include in exception messages. This is not included if it is the same as input. 160 | * @return input upon a successful check 161 | * @throws ValidationException if the check fails. 162 | */ 163 | private String checkBlacklist(String context, String input, String orig) throws ValidationException { 164 | // check blacklist patterns 165 | for (Pattern p : blacklistPatterns) { 166 | if (p.matcher(input).matches()) { 167 | throw new ValidationException(context + ": Invalid input. Dangerous input matching " + p.pattern() + " detected.", "Dangerous input: context=" + context + ", type(" + getTypeName() + ")=" + p.pattern() + ", input=" + input + (NullSafe.equals(orig, input) ? "" : ", orig=" + orig), context); 168 | } 169 | } 170 | 171 | return input; 172 | } 173 | 174 | /** 175 | * checks input against blacklists. 176 | * 177 | * @param context The context to include in exception messages 178 | * @param input the input to check 179 | * @return input upon a successful check 180 | * @throws ValidationException if the check fails. 181 | */ 182 | private String checkBlacklist(String context, String input) throws ValidationException { 183 | return checkBlacklist(context, input, input); 184 | } 185 | 186 | /** 187 | * checks input lengths 188 | * 189 | * @param context The context to include in exception messages 190 | * @param input the input to check 191 | * @param orig A origional input to include in exception messages. This is not included if it is the same as input. 192 | * @return input upon a successful check 193 | * @throws ValidationException if the check fails. 194 | */ 195 | private String checkLength(String context, String input, String orig) throws ValidationException { 196 | if (input.length() < minLength) { 197 | throw new ValidationException(context + ": Invalid input. The minimum length of " + minLength + " characters was not met.", "Input does not meet the minimum length of " + minLength + " by " + (minLength - input.length()) + " characters: context=" + context + ", type=" + getTypeName() + "), input=" + input + (NullSafe.equals(input, orig) ? "" : ", orig=" + orig), context); 198 | } 199 | 200 | if (input.length() > maxLength) { 201 | throw new ValidationException(context + ": Invalid input. The maximum length of " + maxLength + " characters was exceeded.", "Input exceeds maximum allowed length of " + maxLength + " by " + (input.length() - maxLength) + " characters: context=" + context + ", type=" + getTypeName() + ", orig=" + orig + ", input=" + input, context); 202 | } 203 | 204 | return input; 205 | } 206 | 207 | /** 208 | * checks input lengths 209 | * 210 | * @param context The context to include in exception messages 211 | * @param input the input to check 212 | * @return input upon a successful check 213 | * @throws ValidationException if the check fails. 214 | */ 215 | private String checkLength(String context, String input) throws ValidationException { 216 | return checkLength(context, input, input); 217 | } 218 | 219 | /** 220 | * checks input emptiness 221 | * 222 | * @param context The context to include in exception messages 223 | * @param input the input to check 224 | * @param orig A origional input to include in exception messages. This is not included if it is the same as input. 225 | * @return input upon a successful check 226 | * @throws ValidationException if the check fails. 227 | */ 228 | private String checkEmpty(String context, String input, String orig) throws ValidationException { 229 | if (!Utils.isEmpty(input)) { 230 | return input; 231 | } 232 | if (allowNull) { 233 | return null; 234 | } 235 | throw new ValidationException(context + ": Input required.", "Input required: context=" + context + "), input=" + input + (NullSafe.equals(input, orig) ? "" : ", orig=" + orig), context); 236 | } 237 | 238 | /** 239 | * checks input emptiness 240 | * 241 | * @param context The context to include in exception messages 242 | * @param input the input to check 243 | * @return input upon a successful check 244 | * @throws ValidationException if the check fails. 245 | */ 246 | private String checkEmpty(String context, String input) throws ValidationException { 247 | return checkEmpty(context, input, input); 248 | } 249 | 250 | /** 251 | * {@inheritDoc} 252 | */ 253 | public String getValid(String context, String input) throws ValidationException { 254 | String data = null; 255 | 256 | // checks on input itself 257 | 258 | // check for empty/null 259 | if (checkEmpty(context, input) == null) { 260 | return null; 261 | } 262 | 263 | if (validateInputAndCanonical) { 264 | //first validate pre-canonicalized data 265 | 266 | // check length 267 | checkLength(context, input); 268 | 269 | // check whitelist patterns 270 | checkWhitelist(context, input); 271 | 272 | // check blacklist patterns 273 | checkBlacklist(context, input); 274 | 275 | // canonicalize 276 | data = encoder.canonicalize(input); 277 | 278 | } else { 279 | 280 | //skip canonicalization 281 | data = input; 282 | } 283 | 284 | // check for empty/null 285 | if (checkEmpty(context, data, input) == null) { 286 | return null; 287 | } 288 | 289 | // check length 290 | checkLength(context, data, input); 291 | 292 | // check whitelist patterns 293 | checkWhitelist(context, data, input); 294 | 295 | // check blacklist patterns 296 | checkBlacklist(context, data, input); 297 | 298 | // validation passed 299 | return data; 300 | } 301 | 302 | public String sanitize(String context, String input) { 303 | return whitelist(input, Encoder.CHAR_ALPHANUMERICS); 304 | } 305 | 306 | /** 307 | * {@inheritDoc} 308 | */ 309 | public String whitelist(String input, char[] whitelist) { 310 | Set whiteSet = Utils.arrayToSet(whitelist); 311 | return whitelist(input, whiteSet); 312 | } 313 | 314 | /** 315 | * Removes characters that aren't in the whitelist from the input String. O(input.length) whitelist performance 316 | * 317 | * @param input String to be sanitized 318 | * @param whitelist allowed characters 319 | * @return input stripped of all chars that aren't in the whitelist 320 | */ 321 | public String whitelist(String input, Set whitelist) { 322 | StringBuilder stripped = new StringBuilder(); 323 | for (int i = 0; i < input.length(); i++) { 324 | char c = input.charAt(i); 325 | if (whitelist.contains(c)) { 326 | stripped.append(c); 327 | } 328 | } 329 | return stripped.toString(); 330 | } 331 | 332 | public String getTypeName() { 333 | return typeName; 334 | } 335 | 336 | public Encoder getEncoder() { 337 | return encoder; 338 | } 339 | 340 | public boolean isAllowNull() { 341 | return allowNull; 342 | } 343 | 344 | public void setAllowNull(boolean allowNull) { 345 | this.allowNull = allowNull; 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/ValidationException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio; 14 | 15 | /** 16 | * A ValidationException should be thrown to indicate that the data provided by the user or from some other external source does not match the validation rules that have been specified for that data. 17 | */ 18 | public class ValidationException extends Exception { 19 | 20 | protected static final long serialVersionUID = 1L; 21 | /** 22 | * The UI reference that caused this ValidationException 23 | */ 24 | private String context; 25 | /** 26 | * 27 | */ 28 | protected String logMessage = null; 29 | 30 | /** 31 | * Instantiates a new validation exception. 32 | */ 33 | protected ValidationException() { 34 | // hidden 35 | } 36 | 37 | /** 38 | * Creates a new instance of ValidationException. 39 | * 40 | * @param userMessage the message to display to users 41 | * @param logMessage the message logged 42 | */ 43 | public ValidationException(String userMessage, String logMessage) { 44 | super(userMessage); 45 | this.logMessage = logMessage; 46 | } 47 | 48 | /** 49 | * Instantiates a new ValidationException. 50 | * 51 | * @param userMessage the message to display to users 52 | * @param logMessage the message logged 53 | * @param cause the cause 54 | */ 55 | public ValidationException(String userMessage, String logMessage, Throwable cause) { 56 | super(userMessage, cause); 57 | this.logMessage = logMessage; 58 | } 59 | 60 | /** 61 | * Creates a new instance of ValidationException. 62 | * 63 | * @param userMessage the message to display to users 64 | * @param logMessage the message logged 65 | * @param context the source that caused this exception 66 | */ 67 | public ValidationException(String userMessage, String logMessage, String context) { 68 | super(userMessage); 69 | this.logMessage = logMessage; 70 | setContext(context); 71 | } 72 | 73 | /** 74 | * Instantiates a new ValidationException. 75 | * 76 | * @param userMessage the message to display to users 77 | * @param logMessage the message logged 78 | * @param cause the cause 79 | * @param context the source that caused this exception 80 | */ 81 | public ValidationException(String userMessage, String logMessage, Throwable cause, String context) { 82 | super(userMessage, cause); 83 | this.logMessage = logMessage; 84 | setContext(context); 85 | } 86 | 87 | /** 88 | * Returns the UI reference that caused this ValidationException 89 | * 90 | * @return context, the source that caused the exception, stored as a string 91 | */ 92 | public String getContext() { 93 | return context; 94 | } 95 | 96 | /** 97 | * Set's the UI reference that caused this ValidationException 98 | * 99 | * @param context the context to set, passed as a String 100 | */ 101 | protected void setContext(String context) { 102 | this.context = context; 103 | } 104 | 105 | /** 106 | * Returns the UI reference that caused this ValidationException 107 | * 108 | * @return context, the source that caused the exception, stored as a string 109 | */ 110 | public String getLogMessage() { 111 | return logMessage; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/Codec.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.codecs; 14 | 15 | /** 16 | * The Codec interface defines a set of methods for encoding and decoding application level encoding schemes, such as HTML entity encoding and percent encoding (aka URL encoding). Codecs are used in 17 | * output encoding and canonicalization. The design of these codecs allows for character-by-character decoding, which is necessary to detect double-encoding and the use of multiple encoding schemes, 18 | * both of which are techniques used by attackers to bypass validation and bury encoded attacks in data. 19 | */ 20 | public abstract class Codec { 21 | 22 | /** 23 | * Initialize an array to mark which characters are to be encoded. Store the hex string for that character to save time later. If the character shouldn't be encoded, then store null. 24 | */ 25 | private static final String[] hex = new String[256]; 26 | 27 | static { 28 | for (char c = 0; c < 0xFF; c++) { 29 | if (c >= 0x30 && c <= 0x39 || c >= 0x41 && c <= 0x5A || c >= 0x61 && c <= 0x7A) { 30 | hex[c] = null; 31 | } else { 32 | hex[c] = toHex(c).intern(); 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * Default constructor 39 | */ 40 | public Codec() { 41 | } 42 | 43 | /** 44 | * Encode a String so that it can be safely used in a specific context. 45 | * 46 | * @param immune An array of characters that will not be encoded. 47 | * @param input the String to encode 48 | * @return the encoded String 49 | */ 50 | public String encode(char[] immune, String input) { 51 | StringBuilder sb = new StringBuilder(); 52 | for (int i = 0; i < input.length(); i++) { 53 | char c = input.charAt(i); 54 | sb.append(encodeCharacter(immune, c)); 55 | } 56 | return sb.toString(); 57 | } 58 | 59 | /** 60 | * Default implementation that should be overridden in specific codecs. 61 | * 62 | * @param immune An array of characters that will not be encoded. 63 | * @param c the Character to encode 64 | * @return the encoded Character 65 | */ 66 | public String encodeCharacter(char[] immune, Character c) { 67 | return "" + c; 68 | } 69 | 70 | /** 71 | * Decode a String that was encoded using the encode method in this Class 72 | * 73 | * @param input the String to decode 74 | * @return the decoded String 75 | */ 76 | public String decode(String input) { 77 | StringBuilder sb = new StringBuilder(); 78 | PushbackString pbs = new PushbackString(input); 79 | while (pbs.hasNext()) { 80 | Character c = decodeCharacter(pbs); 81 | if (c != null) { 82 | sb.append(c); 83 | } else { 84 | sb.append(pbs.next()); 85 | } 86 | } 87 | return sb.toString(); 88 | } 89 | 90 | /** 91 | * Returns the decoded version of the next character from the input string and advances the current character in the PushbackString. If the current character is not encoded, this method MUST reset 92 | * the PushbackString. 93 | * 94 | * @param input the Character to decode 95 | * 96 | * @return the decoded Character 97 | */ 98 | public Character decodeCharacter(PushbackString input) { 99 | return input.next(); 100 | } 101 | 102 | /** 103 | * Lookup the hex value of any character that is not alphanumeric. 104 | * 105 | * @param c The character to lookup. 106 | * @return, return null if alphanumeric or the character code in hex. 107 | */ 108 | public static String getHexForNonAlphanumeric(char c) { 109 | if (c < 0xFF) { 110 | return hex[c]; 111 | } 112 | return toHex(c); 113 | } 114 | 115 | public static String toOctal(char c) { 116 | return Integer.toOctalString(c); 117 | } 118 | 119 | public static String toHex(char c) { 120 | return Integer.toHexString(c); 121 | } 122 | 123 | /** 124 | * Utility to search a char[] for a specific char. 125 | * 126 | * @param c The character to search for 127 | * @param array The array of characters to search 128 | * @return true if the array contains the character to search for 129 | */ 130 | public static boolean containsCharacter(char c, char[] array) { 131 | for (char ch : array) { 132 | if (c == ch) { 133 | return true; 134 | } 135 | } 136 | return false; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/HTMLEntityCodec.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.codecs; 14 | 15 | import java.util.HashMap; 16 | import java.util.Collections; 17 | import java.util.Map; 18 | 19 | /** 20 | * Implementation of the Codec interface for HTML entity encoding. 21 | */ 22 | public class HTMLEntityCodec extends Codec { 23 | 24 | private static final char REPLACEMENT_CHAR = '\ufffd'; 25 | private static final String REPLACEMENT_HEX = "fffd"; 26 | @SuppressWarnings("unused") 27 | private static final String REPLACEMENT_STR = "" + REPLACEMENT_CHAR; 28 | private static final Map characterToEntityMap = mkCharacterToEntityMap(); 29 | private static final Trie entityToCharacterTrie = mkEntityToCharacterTrie(); 30 | 31 | /** 32 | * 33 | */ 34 | public HTMLEntityCodec() { 35 | } 36 | 37 | /** 38 | * {@inheritDoc} 39 | * 40 | * Encodes a Character for safe use in an HTML entity field. 41 | * 42 | * @param immune An array of characters that will not be encoded. 43 | */ 44 | @Override 45 | public String encodeCharacter(char[] immune, Character c) { 46 | 47 | // check for immune characters 48 | if (containsCharacter(c, immune)) { 49 | return "" + c; 50 | } 51 | 52 | // check for alphanumeric characters 53 | String hex = Codec.getHexForNonAlphanumeric(c); 54 | if (hex == null) { 55 | return "" + c; 56 | } 57 | 58 | // check for illegal characters 59 | if ((c <= 0x1f && c != '\t' && c != '\n' && c != '\r') || (c >= 0x7f && c <= 0x9f)) { 60 | hex = REPLACEMENT_HEX; // Let's entity encode this instead of returning it 61 | c = REPLACEMENT_CHAR; 62 | } 63 | 64 | // check if there's a defined entity 65 | String entityName = characterToEntityMap.get(c); 66 | if (entityName != null) { 67 | return "&" + entityName + ";"; 68 | } 69 | 70 | // return the hex entity as suggested in the spec 71 | return "&#x" + hex + ";"; 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | * 77 | * Returns the decoded version of the character starting at index, or null if no decoding is possible. 78 | * 79 | * Formats all are legal both with and without semi-colon, upper/lower case: &#dddd; &#xhhhh; &name; 80 | */ 81 | @Override 82 | public Character decodeCharacter(PushbackString input) { 83 | input.mark(); 84 | Character first = input.next(); 85 | if (first == null) { 86 | input.reset(); 87 | return null; 88 | } 89 | 90 | // if this is not an encoded character, return null 91 | if (first != '&') { 92 | input.reset(); 93 | return null; 94 | } 95 | 96 | // test for numeric encodings 97 | Character second = input.next(); 98 | if (second == null) { 99 | input.reset(); 100 | return null; 101 | } 102 | 103 | if (second == '#') { 104 | // handle numbers 105 | Character c = getNumericEntity(input); 106 | if (c != null) { 107 | return c; 108 | } 109 | } else if (Character.isLetter(second.charValue())) { 110 | // handle entities 111 | input.pushback(second); 112 | Character c = getNamedEntity(input); 113 | if (c != null) { 114 | return c; 115 | } 116 | } 117 | input.reset(); 118 | return null; 119 | } 120 | 121 | /** 122 | * getNumericEntry checks input to see if it is a numeric entity 123 | * 124 | * @param input The input to test for being a numeric entity 125 | * 126 | * @return null if input is null, the character of input after decoding 127 | */ 128 | private Character getNumericEntity(PushbackString input) { 129 | Character first = input.peek(); 130 | if (first == null) { 131 | return null; 132 | } 133 | 134 | if (first == 'x' || first == 'X') { 135 | input.next(); 136 | return parseHex(input); 137 | } 138 | return parseNumber(input); 139 | } 140 | 141 | /** 142 | * Parse a decimal number, such as those from JavaScript's String.fromCharCode(value) 143 | * 144 | * @param input decimal encoded string, such as 65 145 | * @return character representation of this decimal value, e.g. A 146 | * @throws NumberFormatException 147 | */ 148 | private Character parseNumber(PushbackString input) { 149 | StringBuilder sb = new StringBuilder(); 150 | while (input.hasNext()) { 151 | Character c = input.peek(); 152 | 153 | // if character is a digit then add it on and keep going 154 | if (Character.isDigit(c.charValue())) { 155 | sb.append(c); 156 | input.next(); 157 | 158 | // if character is a semi-colon, eat it and quit 159 | } else if (c == ';') { 160 | input.next(); 161 | break; 162 | 163 | // otherwise just quit 164 | } else { 165 | break; 166 | } 167 | } 168 | try { 169 | int i = Integer.parseInt(sb.toString()); 170 | if (Character.isValidCodePoint(i)) { 171 | return (char) i; 172 | } 173 | } catch (NumberFormatException e) { 174 | // throw an exception for malformed entity? 175 | } 176 | return null; 177 | } 178 | 179 | /** 180 | * Parse a hex encoded entity 181 | * 182 | * @param input Hex encoded input (such as 437ae;) 183 | * @return A single character from the string 184 | * @throws NumberFormatException 185 | */ 186 | private Character parseHex(PushbackString input) { 187 | StringBuilder sb = new StringBuilder(); 188 | while (input.hasNext()) { 189 | Character c = input.peek(); 190 | 191 | // if character is a hex digit then add it on and keep going 192 | if ("0123456789ABCDEFabcdef".indexOf(c) != -1) { 193 | sb.append(c); 194 | input.next(); 195 | 196 | // if character is a semi-colon, eat it and quit 197 | } else if (c == ';') { 198 | input.next(); 199 | break; 200 | 201 | // otherwise just quit 202 | } else { 203 | break; 204 | } 205 | } 206 | try { 207 | int i = Integer.parseInt(sb.toString(), 16); 208 | if (Character.isValidCodePoint(i)) { 209 | return (char) i; 210 | } 211 | } catch (NumberFormatException e) { 212 | // throw an exception for malformed entity? 213 | } 214 | return null; 215 | } 216 | 217 | /** 218 | * 219 | * Returns the decoded version of the character starting at index, or null if no decoding is possible. 220 | * 221 | * Formats all are legal both with and without semi-colon, upper/lower case: &aa; &aaa; &aaaa; &aaaaa; &aaaaaa; &aaaaaaa; 222 | * 223 | * @param input A string containing a named entity like " 224 | * @return Returns the decoded version of the character starting at index, or null if no decoding is possible. 225 | */ 226 | private Character getNamedEntity(PushbackString input) { 227 | StringBuilder possible = new StringBuilder(); 228 | Map.Entry entry; 229 | int len; 230 | 231 | // kludge around PushbackString.... 232 | len = Math.min(input.remainder().length(), entityToCharacterTrie.getMaxKeyLength()); 233 | for (int i = 0; i < len; i++) { 234 | possible.append(Character.toLowerCase(input.next())); 235 | } 236 | 237 | // look up the longest match 238 | entry = entityToCharacterTrie.getLongestMatch(possible); 239 | if (entry == null) { 240 | return null; // no match, caller will reset input 241 | } 242 | // fixup input 243 | input.reset(); 244 | input.next(); // read & 245 | len = entry.getKey().length(); // what matched's length 246 | for (int i = 0; i < len; i++) { 247 | input.next(); 248 | } 249 | 250 | // check for a trailing semicolen 251 | if (input.peek(';')) { 252 | input.next(); 253 | } 254 | 255 | return entry.getValue(); 256 | } 257 | 258 | /** 259 | * Build a unmodifiable Map from entity Character to Name. 260 | * 261 | * @return Unmodifiable map. 262 | */ 263 | private static synchronized Map mkCharacterToEntityMap() { 264 | Map map = new HashMap(252); 265 | 266 | map.put((char) 34, "quot"); /* quotation mark */ 267 | map.put((char) 38, "amp"); /* ampersand */ 268 | map.put((char) 60, "lt"); /* less-than sign */ 269 | map.put((char) 62, "gt"); /* greater-than sign */ 270 | map.put((char) 160, "nbsp"); /* no-break space */ 271 | map.put((char) 161, "iexcl"); /* inverted exclamation mark */ 272 | map.put((char) 162, "cent"); /* cent sign */ 273 | map.put((char) 163, "pound"); /* pound sign */ 274 | map.put((char) 164, "curren"); /* currency sign */ 275 | map.put((char) 165, "yen"); /* yen sign */ 276 | map.put((char) 166, "brvbar"); /* broken bar */ 277 | map.put((char) 167, "sect"); /* section sign */ 278 | map.put((char) 168, "uml"); /* diaeresis */ 279 | map.put((char) 169, "copy"); /* copyright sign */ 280 | map.put((char) 170, "ordf"); /* feminine ordinal indicator */ 281 | map.put((char) 171, "laquo"); /* left-pointing double angle quotation mark */ 282 | map.put((char) 172, "not"); /* not sign */ 283 | map.put((char) 173, "shy"); /* soft hyphen */ 284 | map.put((char) 174, "reg"); /* registered sign */ 285 | map.put((char) 175, "macr"); /* macron */ 286 | map.put((char) 176, "deg"); /* degree sign */ 287 | map.put((char) 177, "plusmn"); /* plus-minus sign */ 288 | map.put((char) 178, "sup2"); /* superscript two */ 289 | map.put((char) 179, "sup3"); /* superscript three */ 290 | map.put((char) 180, "acute"); /* acute accent */ 291 | map.put((char) 181, "micro"); /* micro sign */ 292 | map.put((char) 182, "para"); /* pilcrow sign */ 293 | map.put((char) 183, "middot"); /* middle dot */ 294 | map.put((char) 184, "cedil"); /* cedilla */ 295 | map.put((char) 185, "sup1"); /* superscript one */ 296 | map.put((char) 186, "ordm"); /* masculine ordinal indicator */ 297 | map.put((char) 187, "raquo"); /* right-pointing double angle quotation mark */ 298 | map.put((char) 188, "frac14"); /* vulgar fraction one quarter */ 299 | map.put((char) 189, "frac12"); /* vulgar fraction one half */ 300 | map.put((char) 190, "frac34"); /* vulgar fraction three quarters */ 301 | map.put((char) 191, "iquest"); /* inverted question mark */ 302 | map.put((char) 192, "Agrave"); /* Latin capital letter a with grave */ 303 | map.put((char) 193, "Aacute"); /* Latin capital letter a with acute */ 304 | map.put((char) 194, "Acirc"); /* Latin capital letter a with circumflex */ 305 | map.put((char) 195, "Atilde"); /* Latin capital letter a with tilde */ 306 | map.put((char) 196, "Auml"); /* Latin capital letter a with diaeresis */ 307 | map.put((char) 197, "Aring"); /* Latin capital letter a with ring above */ 308 | map.put((char) 198, "AElig"); /* Latin capital letter ae */ 309 | map.put((char) 199, "Ccedil"); /* Latin capital letter c with cedilla */ 310 | map.put((char) 200, "Egrave"); /* Latin capital letter e with grave */ 311 | map.put((char) 201, "Eacute"); /* Latin capital letter e with acute */ 312 | map.put((char) 202, "Ecirc"); /* Latin capital letter e with circumflex */ 313 | map.put((char) 203, "Euml"); /* Latin capital letter e with diaeresis */ 314 | map.put((char) 204, "Igrave"); /* Latin capital letter i with grave */ 315 | map.put((char) 205, "Iacute"); /* Latin capital letter i with acute */ 316 | map.put((char) 206, "Icirc"); /* Latin capital letter i with circumflex */ 317 | map.put((char) 207, "Iuml"); /* Latin capital letter i with diaeresis */ 318 | map.put((char) 208, "ETH"); /* Latin capital letter eth */ 319 | map.put((char) 209, "Ntilde"); /* Latin capital letter n with tilde */ 320 | map.put((char) 210, "Ograve"); /* Latin capital letter o with grave */ 321 | map.put((char) 211, "Oacute"); /* Latin capital letter o with acute */ 322 | map.put((char) 212, "Ocirc"); /* Latin capital letter o with circumflex */ 323 | map.put((char) 213, "Otilde"); /* Latin capital letter o with tilde */ 324 | map.put((char) 214, "Ouml"); /* Latin capital letter o with diaeresis */ 325 | map.put((char) 215, "times"); /* multiplication sign */ 326 | map.put((char) 216, "Oslash"); /* Latin capital letter o with stroke */ 327 | map.put((char) 217, "Ugrave"); /* Latin capital letter u with grave */ 328 | map.put((char) 218, "Uacute"); /* Latin capital letter u with acute */ 329 | map.put((char) 219, "Ucirc"); /* Latin capital letter u with circumflex */ 330 | map.put((char) 220, "Uuml"); /* Latin capital letter u with diaeresis */ 331 | map.put((char) 221, "Yacute"); /* Latin capital letter y with acute */ 332 | map.put((char) 222, "THORN"); /* Latin capital letter thorn */ 333 | map.put((char) 223, "szlig"); /* Latin small letter sharp sXCOMMAX German Eszett */ 334 | map.put((char) 224, "agrave"); /* Latin small letter a with grave */ 335 | map.put((char) 225, "aacute"); /* Latin small letter a with acute */ 336 | map.put((char) 226, "acirc"); /* Latin small letter a with circumflex */ 337 | map.put((char) 227, "atilde"); /* Latin small letter a with tilde */ 338 | map.put((char) 228, "auml"); /* Latin small letter a with diaeresis */ 339 | map.put((char) 229, "aring"); /* Latin small letter a with ring above */ 340 | map.put((char) 230, "aelig"); /* Latin lowercase ligature ae */ 341 | map.put((char) 231, "ccedil"); /* Latin small letter c with cedilla */ 342 | map.put((char) 232, "egrave"); /* Latin small letter e with grave */ 343 | map.put((char) 233, "eacute"); /* Latin small letter e with acute */ 344 | map.put((char) 234, "ecirc"); /* Latin small letter e with circumflex */ 345 | map.put((char) 235, "euml"); /* Latin small letter e with diaeresis */ 346 | map.put((char) 236, "igrave"); /* Latin small letter i with grave */ 347 | map.put((char) 237, "iacute"); /* Latin small letter i with acute */ 348 | map.put((char) 238, "icirc"); /* Latin small letter i with circumflex */ 349 | map.put((char) 239, "iuml"); /* Latin small letter i with diaeresis */ 350 | map.put((char) 240, "eth"); /* Latin small letter eth */ 351 | map.put((char) 241, "ntilde"); /* Latin small letter n with tilde */ 352 | map.put((char) 242, "ograve"); /* Latin small letter o with grave */ 353 | map.put((char) 243, "oacute"); /* Latin small letter o with acute */ 354 | map.put((char) 244, "ocirc"); /* Latin small letter o with circumflex */ 355 | map.put((char) 245, "otilde"); /* Latin small letter o with tilde */ 356 | map.put((char) 246, "ouml"); /* Latin small letter o with diaeresis */ 357 | map.put((char) 247, "divide"); /* division sign */ 358 | map.put((char) 248, "oslash"); /* Latin small letter o with stroke */ 359 | map.put((char) 249, "ugrave"); /* Latin small letter u with grave */ 360 | map.put((char) 250, "uacute"); /* Latin small letter u with acute */ 361 | map.put((char) 251, "ucirc"); /* Latin small letter u with circumflex */ 362 | map.put((char) 252, "uuml"); /* Latin small letter u with diaeresis */ 363 | map.put((char) 253, "yacute"); /* Latin small letter y with acute */ 364 | map.put((char) 254, "thorn"); /* Latin small letter thorn */ 365 | map.put((char) 255, "yuml"); /* Latin small letter y with diaeresis */ 366 | map.put((char) 338, "OElig"); /* Latin capital ligature oe */ 367 | map.put((char) 339, "oelig"); /* Latin small ligature oe */ 368 | map.put((char) 352, "Scaron"); /* Latin capital letter s with caron */ 369 | map.put((char) 353, "scaron"); /* Latin small letter s with caron */ 370 | map.put((char) 376, "Yuml"); /* Latin capital letter y with diaeresis */ 371 | map.put((char) 402, "fnof"); /* Latin small letter f with hook */ 372 | map.put((char) 710, "circ"); /* modifier letter circumflex accent */ 373 | map.put((char) 732, "tilde"); /* small tilde */ 374 | map.put((char) 913, "Alpha"); /* Greek capital letter alpha */ 375 | map.put((char) 914, "Beta"); /* Greek capital letter beta */ 376 | map.put((char) 915, "Gamma"); /* Greek capital letter gamma */ 377 | map.put((char) 916, "Delta"); /* Greek capital letter delta */ 378 | map.put((char) 917, "Epsilon"); /* Greek capital letter epsilon */ 379 | map.put((char) 918, "Zeta"); /* Greek capital letter zeta */ 380 | map.put((char) 919, "Eta"); /* Greek capital letter eta */ 381 | map.put((char) 920, "Theta"); /* Greek capital letter theta */ 382 | map.put((char) 921, "Iota"); /* Greek capital letter iota */ 383 | map.put((char) 922, "Kappa"); /* Greek capital letter kappa */ 384 | map.put((char) 923, "Lambda"); /* Greek capital letter lambda */ 385 | map.put((char) 924, "Mu"); /* Greek capital letter mu */ 386 | map.put((char) 925, "Nu"); /* Greek capital letter nu */ 387 | map.put((char) 926, "Xi"); /* Greek capital letter xi */ 388 | map.put((char) 927, "Omicron"); /* Greek capital letter omicron */ 389 | map.put((char) 928, "Pi"); /* Greek capital letter pi */ 390 | map.put((char) 929, "Rho"); /* Greek capital letter rho */ 391 | map.put((char) 931, "Sigma"); /* Greek capital letter sigma */ 392 | map.put((char) 932, "Tau"); /* Greek capital letter tau */ 393 | map.put((char) 933, "Upsilon"); /* Greek capital letter upsilon */ 394 | map.put((char) 934, "Phi"); /* Greek capital letter phi */ 395 | map.put((char) 935, "Chi"); /* Greek capital letter chi */ 396 | map.put((char) 936, "Psi"); /* Greek capital letter psi */ 397 | map.put((char) 937, "Omega"); /* Greek capital letter omega */ 398 | map.put((char) 945, "alpha"); /* Greek small letter alpha */ 399 | map.put((char) 946, "beta"); /* Greek small letter beta */ 400 | map.put((char) 947, "gamma"); /* Greek small letter gamma */ 401 | map.put((char) 948, "delta"); /* Greek small letter delta */ 402 | map.put((char) 949, "epsilon"); /* Greek small letter epsilon */ 403 | map.put((char) 950, "zeta"); /* Greek small letter zeta */ 404 | map.put((char) 951, "eta"); /* Greek small letter eta */ 405 | map.put((char) 952, "theta"); /* Greek small letter theta */ 406 | map.put((char) 953, "iota"); /* Greek small letter iota */ 407 | map.put((char) 954, "kappa"); /* Greek small letter kappa */ 408 | map.put((char) 955, "lambda"); /* Greek small letter lambda */ 409 | map.put((char) 956, "mu"); /* Greek small letter mu */ 410 | map.put((char) 957, "nu"); /* Greek small letter nu */ 411 | map.put((char) 958, "xi"); /* Greek small letter xi */ 412 | map.put((char) 959, "omicron"); /* Greek small letter omicron */ 413 | map.put((char) 960, "pi"); /* Greek small letter pi */ 414 | map.put((char) 961, "rho"); /* Greek small letter rho */ 415 | map.put((char) 962, "sigmaf"); /* Greek small letter final sigma */ 416 | map.put((char) 963, "sigma"); /* Greek small letter sigma */ 417 | map.put((char) 964, "tau"); /* Greek small letter tau */ 418 | map.put((char) 965, "upsilon"); /* Greek small letter upsilon */ 419 | map.put((char) 966, "phi"); /* Greek small letter phi */ 420 | map.put((char) 967, "chi"); /* Greek small letter chi */ 421 | map.put((char) 968, "psi"); /* Greek small letter psi */ 422 | map.put((char) 969, "omega"); /* Greek small letter omega */ 423 | map.put((char) 977, "thetasym"); /* Greek theta symbol */ 424 | map.put((char) 978, "upsih"); /* Greek upsilon with hook symbol */ 425 | map.put((char) 982, "piv"); /* Greek pi symbol */ 426 | map.put((char) 8194, "ensp"); /* en space */ 427 | map.put((char) 8195, "emsp"); /* em space */ 428 | map.put((char) 8201, "thinsp"); /* thin space */ 429 | map.put((char) 8204, "zwnj"); /* zero width non-joiner */ 430 | map.put((char) 8205, "zwj"); /* zero width joiner */ 431 | map.put((char) 8206, "lrm"); /* left-to-right mark */ 432 | map.put((char) 8207, "rlm"); /* right-to-left mark */ 433 | map.put((char) 8211, "ndash"); /* en dash */ 434 | map.put((char) 8212, "mdash"); /* em dash */ 435 | map.put((char) 8216, "lsquo"); /* left single quotation mark */ 436 | map.put((char) 8217, "rsquo"); /* right single quotation mark */ 437 | map.put((char) 8218, "sbquo"); /* single low-9 quotation mark */ 438 | map.put((char) 8220, "ldquo"); /* left double quotation mark */ 439 | map.put((char) 8221, "rdquo"); /* right double quotation mark */ 440 | map.put((char) 8222, "bdquo"); /* double low-9 quotation mark */ 441 | map.put((char) 8224, "dagger"); /* dagger */ 442 | map.put((char) 8225, "Dagger"); /* double dagger */ 443 | map.put((char) 8226, "bull"); /* bullet */ 444 | map.put((char) 8230, "hellip"); /* horizontal ellipsis */ 445 | map.put((char) 8240, "permil"); /* per mille sign */ 446 | map.put((char) 8242, "prime"); /* prime */ 447 | map.put((char) 8243, "Prime"); /* double prime */ 448 | map.put((char) 8249, "lsaquo"); /* single left-pointing angle quotation mark */ 449 | map.put((char) 8250, "rsaquo"); /* single right-pointing angle quotation mark */ 450 | map.put((char) 8254, "oline"); /* overline */ 451 | map.put((char) 8260, "frasl"); /* fraction slash */ 452 | map.put((char) 8364, "euro"); /* euro sign */ 453 | map.put((char) 8465, "image"); /* black-letter capital i */ 454 | map.put((char) 8472, "weierp"); /* script capital pXCOMMAX Weierstrass p */ 455 | map.put((char) 8476, "real"); /* black-letter capital r */ 456 | map.put((char) 8482, "trade"); /* trademark sign */ 457 | map.put((char) 8501, "alefsym"); /* alef symbol */ 458 | map.put((char) 8592, "larr"); /* leftwards arrow */ 459 | map.put((char) 8593, "uarr"); /* upwards arrow */ 460 | map.put((char) 8594, "rarr"); /* rightwards arrow */ 461 | map.put((char) 8595, "darr"); /* downwards arrow */ 462 | map.put((char) 8596, "harr"); /* left right arrow */ 463 | map.put((char) 8629, "crarr"); /* downwards arrow with corner leftwards */ 464 | map.put((char) 8656, "lArr"); /* leftwards double arrow */ 465 | map.put((char) 8657, "uArr"); /* upwards double arrow */ 466 | map.put((char) 8658, "rArr"); /* rightwards double arrow */ 467 | map.put((char) 8659, "dArr"); /* downwards double arrow */ 468 | map.put((char) 8660, "hArr"); /* left right double arrow */ 469 | map.put((char) 8704, "forall"); /* for all */ 470 | map.put((char) 8706, "part"); /* partial differential */ 471 | map.put((char) 8707, "exist"); /* there exists */ 472 | map.put((char) 8709, "empty"); /* empty set */ 473 | map.put((char) 8711, "nabla"); /* nabla */ 474 | map.put((char) 8712, "isin"); /* element of */ 475 | map.put((char) 8713, "notin"); /* not an element of */ 476 | map.put((char) 8715, "ni"); /* contains as member */ 477 | map.put((char) 8719, "prod"); /* n-ary product */ 478 | map.put((char) 8721, "sum"); /* n-ary summation */ 479 | map.put((char) 8722, "minus"); /* minus sign */ 480 | map.put((char) 8727, "lowast"); /* asterisk operator */ 481 | map.put((char) 8730, "radic"); /* square root */ 482 | map.put((char) 8733, "prop"); /* proportional to */ 483 | map.put((char) 8734, "infin"); /* infinity */ 484 | map.put((char) 8736, "ang"); /* angle */ 485 | map.put((char) 8743, "and"); /* logical and */ 486 | map.put((char) 8744, "or"); /* logical or */ 487 | map.put((char) 8745, "cap"); /* intersection */ 488 | map.put((char) 8746, "cup"); /* union */ 489 | map.put((char) 8747, "int"); /* integral */ 490 | map.put((char) 8756, "there4"); /* therefore */ 491 | map.put((char) 8764, "sim"); /* tilde operator */ 492 | map.put((char) 8773, "cong"); /* congruent to */ 493 | map.put((char) 8776, "asymp"); /* almost equal to */ 494 | map.put((char) 8800, "ne"); /* not equal to */ 495 | map.put((char) 8801, "equiv"); /* identical toXCOMMAX equivalent to */ 496 | map.put((char) 8804, "le"); /* less-than or equal to */ 497 | map.put((char) 8805, "ge"); /* greater-than or equal to */ 498 | map.put((char) 8834, "sub"); /* subset of */ 499 | map.put((char) 8835, "sup"); /* superset of */ 500 | map.put((char) 8836, "nsub"); /* not a subset of */ 501 | map.put((char) 8838, "sube"); /* subset of or equal to */ 502 | map.put((char) 8839, "supe"); /* superset of or equal to */ 503 | map.put((char) 8853, "oplus"); /* circled plus */ 504 | map.put((char) 8855, "otimes"); /* circled times */ 505 | map.put((char) 8869, "perp"); /* up tack */ 506 | map.put((char) 8901, "sdot"); /* dot operator */ 507 | map.put((char) 8968, "lceil"); /* left ceiling */ 508 | map.put((char) 8969, "rceil"); /* right ceiling */ 509 | map.put((char) 8970, "lfloor"); /* left floor */ 510 | map.put((char) 8971, "rfloor"); /* right floor */ 511 | map.put((char) 9001, "lang"); /* left-pointing angle bracket */ 512 | map.put((char) 9002, "rang"); /* right-pointing angle bracket */ 513 | map.put((char) 9674, "loz"); /* lozenge */ 514 | map.put((char) 9824, "spades"); /* black spade suit */ 515 | map.put((char) 9827, "clubs"); /* black club suit */ 516 | map.put((char) 9829, "hearts"); /* black heart suit */ 517 | map.put((char) 9830, "diams"); /* black diamond suit */ 518 | 519 | return Collections.unmodifiableMap(map); 520 | } 521 | 522 | /** 523 | * Build a unmodifiable Trie from entitiy Name to Character 524 | * 525 | * @return Unmodifiable trie. 526 | */ 527 | private static synchronized Trie mkEntityToCharacterTrie() { 528 | Trie trie = new HashTrie(); 529 | 530 | for (Map.Entry entry : characterToEntityMap.entrySet()) { 531 | trie.put(entry.getValue(), entry.getKey()); 532 | } 533 | return Trie.Util.unmodifiable(trie); 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/HashTrie.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Ed Schaller - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.codecs; 14 | 15 | import java.io.IOException; 16 | import java.io.PushbackReader; 17 | import java.util.ArrayList; 18 | import java.util.Collection; 19 | import java.util.HashMap; 20 | import java.util.HashSet; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | import org.owasp.fileio.util.NullSafe; 25 | 26 | /** 27 | * Trie implementation for CharSequence keys. This uses HashMaps for each level instead of the traditional array. This is done as with unicode, each level's array would be 64k entries. 28 | * 29 | * NOTE:
30 | *
    31 | *
  • java.util.Map.remove(java.lang.Object) is not supported.
  • 32 | *
  • 33 | * If deletion support is added the max key length will need work or removal. 34 | *
  • 35 | *
  • Null values are not supported.
  • 36 | *
37 | * 38 | * @author Ed Schaller 39 | */ 40 | public class HashTrie implements Trie { 41 | 42 | private static class Entry implements Map.Entry { 43 | 44 | private CharSequence key; 45 | private T value; 46 | 47 | Entry(CharSequence key, T value) { 48 | this.key = key; 49 | this.value = value; 50 | } 51 | 52 | /** 53 | * Convinence instantiator. 54 | * 55 | * @param key The key for the new instance 56 | * @param keyLength The length of the key to use 57 | * @param value The value for the new instance 58 | * @return null if key or value is null new Entry(key,value) if {@link CharSequence#length()} == keyLength new Entry(key.subSequence(0,keyLength),value) otherwise 59 | */ 60 | static Entry newInstanceIfNeeded(CharSequence key, int keyLength, T value) { 61 | if (value == null || key == null) { 62 | return null; 63 | } 64 | if (key.length() > keyLength) { 65 | key = key.subSequence(0, keyLength); 66 | } 67 | return new Entry(key, value); 68 | } 69 | 70 | /** 71 | * Convinence instantiator. 72 | * 73 | * @param key The key for the new instance 74 | * @param value The value for the new instance 75 | * @return null if key or value is null new Entry(key,value) otherwise 76 | */ 77 | static Entry newInstanceIfNeeded(CharSequence key, T value) { 78 | if (value == null || key == null) { 79 | return null; 80 | } 81 | return new Entry(key, value); 82 | } 83 | 84 | /** 85 | * ********** 86 | */ 87 | /* Map.Entry */ 88 | /** 89 | * ********** 90 | */ 91 | public CharSequence getKey() { 92 | return key; 93 | } 94 | 95 | public T getValue() { 96 | return value; 97 | } 98 | 99 | public T setValue(T value) { 100 | throw new UnsupportedOperationException(); 101 | } 102 | 103 | /** 104 | * ***************** 105 | */ 106 | /* java.lang.Object */ 107 | /** 108 | * ***************** 109 | */ 110 | public boolean equals(Map.Entry other) { 111 | return (NullSafe.equals(key, other.getKey()) && NullSafe.equals(value, other.getValue())); 112 | } 113 | 114 | @SuppressWarnings("unchecked") 115 | @Override 116 | public boolean equals(Object o) { 117 | if (o instanceof Map.Entry) { 118 | return equals((Map.Entry) o); 119 | } 120 | return false; 121 | } 122 | 123 | @Override 124 | public int hashCode() { 125 | return NullSafe.hashCode(key) ^ NullSafe.hashCode(value); 126 | } 127 | 128 | @Override 129 | public String toString() { 130 | return NullSafe.toString(key) + " => " + NullSafe.toString(value); 131 | } 132 | } 133 | 134 | /** 135 | * Node inside the trie. 136 | */ 137 | private static class Node { 138 | 139 | private T value = null; 140 | private Map> nextMap; 141 | 142 | /** 143 | * Create a new Map for a node level. This is here so that if the underlying * Map implmentation needs to be switched it is easily done. 144 | * 145 | * @return A new Map for use. 146 | */ 147 | private static Map> newNodeMap() { 148 | return new HashMap>(); 149 | } 150 | 151 | /** 152 | * Create a new Map for a node level. This is here so that if the underlying * Map implmentation needs to be switched it is easily done. 153 | * 154 | * @param prev Pervious map to use to populate the new map. 155 | * @return A new Map for use. 156 | */ 157 | private static Map> newNodeMap(Map> prev) { 158 | return new HashMap>(prev); 159 | } 160 | 161 | /** 162 | * Set the value for the key terminated at this node. 163 | * 164 | * @param value The value for this key. 165 | */ 166 | void setValue(T value) { 167 | this.value = value; 168 | } 169 | 170 | /** 171 | * Get the node for the specified character. 172 | * 173 | * @param ch The next character to look for. 174 | * @return The node requested or null if it is not present. 175 | */ 176 | Node getNextNode(Character ch) { 177 | if (nextMap == null) { 178 | return null; 179 | } 180 | return nextMap.get(ch); 181 | } 182 | 183 | /** 184 | * Recursively add a key. 185 | * 186 | * @param key The key being added. 187 | * @param pos The position in key that is being handled at this level. 188 | */ 189 | T put(CharSequence key, int pos, T addValue) { 190 | Node nextNode; 191 | Character ch; 192 | T old; 193 | 194 | if (key.length() == pos) { // at terminating node 195 | old = value; 196 | setValue(addValue); 197 | return old; 198 | } 199 | ch = key.charAt(pos); 200 | if (nextMap == null) { 201 | nextMap = newNodeMap(); 202 | nextNode = new Node<>(); 203 | nextMap.put(ch, nextNode); 204 | } else if ((nextNode = nextMap.get(ch)) == null) { 205 | nextNode = new Node<>(); 206 | nextMap.put(ch, nextNode); 207 | } 208 | return nextNode.put(key, pos + 1, addValue); 209 | } 210 | 211 | /** 212 | * Recursively lookup a key's value. 213 | * 214 | * @param key The key being looked up. 215 | * @param pos The position in the key that is being looked up at this level. 216 | * @return The value assocatied with the key or null if none exists. 217 | */ 218 | T get(CharSequence key, int pos) { 219 | Node nextNode; 220 | 221 | if (key.length() <= pos) // <= instead of == just in case 222 | { 223 | return value; // no value is null which is also not found 224 | } 225 | if ((nextNode = getNextNode(key.charAt(pos))) == null) { 226 | return null; 227 | } 228 | return nextNode.get(key, pos + 1); 229 | } 230 | 231 | /** 232 | * Recursively lookup the longest key match. 233 | * 234 | * @param key The key being looked up. 235 | * @param pos The position in the key that is being looked up at this level. 236 | * @return The Entry assocatied with the longest key match or null if none exists. 237 | */ 238 | Entry getLongestMatch(CharSequence key, int pos) { 239 | Node nextNode; 240 | Entry ret; 241 | 242 | if (key.length() <= pos) // <= instead of == just in case 243 | { 244 | return Entry.newInstanceIfNeeded(key, value); 245 | } 246 | if ((nextNode = getNextNode(key.charAt(pos))) == null) { // last in trie... return ourselves 247 | return Entry.newInstanceIfNeeded(key, pos, value); 248 | } 249 | if ((ret = nextNode.getLongestMatch(key, pos + 1)) != null) { 250 | return ret; 251 | } 252 | return Entry.newInstanceIfNeeded(key, pos, value); 253 | } 254 | 255 | /** 256 | * Recursively lookup the longest key match. 257 | * 258 | * @param keyIn Where to read the key from 259 | * @param pos The position in the key that is being looked up at this level. 260 | * @return The Entry assocatied with the longest key match or null if none exists. 261 | */ 262 | Entry getLongestMatch(PushbackReader keyIn, StringBuilder key) throws IOException { 263 | Node nextNode; 264 | Entry ret; 265 | int c; 266 | char ch; 267 | int prevLen; 268 | 269 | // read next key char and append to key... 270 | if ((c = keyIn.read()) < 0) // end of input, return what we have currently 271 | { 272 | return Entry.newInstanceIfNeeded(key, value); 273 | } 274 | ch = (char) c; 275 | prevLen = key.length(); 276 | key.append(ch); 277 | 278 | if ((nextNode = getNextNode(ch)) == null) { // last in trie... return ourselves 279 | return Entry.newInstanceIfNeeded(key, value); 280 | } 281 | if ((ret = nextNode.getLongestMatch(keyIn, key)) != null) { 282 | return ret; 283 | } 284 | 285 | // undo reading of key char and appending to key... 286 | key.setLength(prevLen); 287 | keyIn.unread(c); 288 | 289 | return Entry.newInstanceIfNeeded(key, value); 290 | } 291 | 292 | /** 293 | * Recursively rebuild the internal maps. 294 | */ 295 | @SuppressWarnings("unused") 296 | void remap() { 297 | if (nextMap == null) { 298 | return; 299 | } 300 | nextMap = newNodeMap(nextMap); 301 | for (Node node : nextMap.values()) { 302 | node.remap(); 303 | } 304 | } 305 | 306 | /** 307 | * Recursively search for a value. 308 | * 309 | * @param toFind The value to search for 310 | * @return true if the value was found false otherwise 311 | */ 312 | boolean containsValue(Object toFind) { 313 | if (value != null && toFind.equals(value)) { 314 | return true; 315 | } 316 | if (nextMap == null) { 317 | return false; 318 | } 319 | for (Node node : nextMap.values()) { 320 | if (node.containsValue(toFind)) { 321 | return true; 322 | } 323 | } 324 | return false; 325 | } 326 | 327 | /** 328 | * Recursively build values. 329 | * 330 | * @param values List being built. 331 | * @return true if the value was found false otherwise 332 | */ 333 | Collection values(Collection values) { 334 | if (value != null) { 335 | values.add(value); 336 | } 337 | if (nextMap == null) { 338 | return values; 339 | } 340 | for (Node node : nextMap.values()) { 341 | node.values(values); 342 | } 343 | return values; 344 | } 345 | 346 | /** 347 | * Recursively build a key set. 348 | * 349 | * @param key StringBuilder with our key. 350 | * @param keys Set to add to 351 | * @return keys with additions 352 | */ 353 | Set keySet(StringBuilder key, Set keys) { 354 | int len = key.length(); 355 | 356 | if (value != null) // MUST toString here 357 | { 358 | keys.add(key.toString()); 359 | } 360 | if (nextMap != null && nextMap.size() > 0) { 361 | key.append('X'); 362 | for (Map.Entry> entry : nextMap.entrySet()) { 363 | key.setCharAt(len, entry.getKey()); 364 | entry.getValue().keySet(key, keys); 365 | } 366 | key.setLength(len); 367 | } 368 | return keys; 369 | } 370 | 371 | /** 372 | * Recursively build a entry set. 373 | * 374 | * @param key StringBuilder with our key. 375 | * @param entries Set to add to 376 | * @return entries with additions 377 | */ 378 | Set> entrySet(StringBuilder key, Set> entries) { 379 | int len = key.length(); 380 | 381 | if (value != null) // MUST toString here 382 | { 383 | entries.add(new Entry<>(key.toString(), value)); 384 | } 385 | if (nextMap != null && nextMap.size() > 0) { 386 | key.append('X'); 387 | for (Map.Entry> entry : nextMap.entrySet()) { 388 | key.setCharAt(len, entry.getKey()); 389 | entry.getValue().entrySet(key, entries); 390 | } 391 | key.setLength(len); 392 | } 393 | return entries; 394 | } 395 | } 396 | private Node root; 397 | private int maxKeyLen; 398 | private int size; 399 | 400 | public HashTrie() { 401 | clear(); 402 | } 403 | 404 | /** 405 | * Get the key value entry who's key is the longest prefix match. 406 | * 407 | * @param key The key to lookup 408 | * @return Entry with the longest matching key. 409 | */ 410 | @Override 411 | public Map.Entry getLongestMatch(CharSequence key) { 412 | if (root == null || key == null) { 413 | return null; 414 | } 415 | return root.getLongestMatch(key, 0); 416 | } 417 | 418 | /** 419 | * Get the key value entry who's key is the longest prefix match. 420 | * 421 | * @param keyIn Pushback reader to read the key from. This should have a buffer at least as large as {@link #getMaxKeyLength()} or an IOException may be thrown backing up. 422 | * @return Entry with the longest matching key. 423 | * @throws IOException if keyIn.read() or keyIn.unread() does. 424 | */ 425 | @Override 426 | public Map.Entry getLongestMatch(PushbackReader keyIn) throws IOException { 427 | if (root == null || keyIn == null) { 428 | return null; 429 | } 430 | return root.getLongestMatch(keyIn, new StringBuilder()); 431 | } 432 | 433 | /** 434 | * Get the maximum key length. 435 | * 436 | * @return max key length. 437 | */ 438 | @Override 439 | public int getMaxKeyLength() { 440 | return maxKeyLen; 441 | } 442 | 443 | /** 444 | * ************** 445 | */ 446 | /* java.util.Map */ 447 | /** 448 | * ************** 449 | */ 450 | /** 451 | * Clear all entries. 452 | */ 453 | @Override 454 | public void clear() { 455 | root = null; 456 | maxKeyLen = -1; 457 | size = 0; 458 | } 459 | 460 | /** 461 | * {@inheritDoc} 462 | */ 463 | @Override 464 | public boolean containsKey(Object key) { 465 | return (get(key) != null); 466 | } 467 | 468 | /** 469 | * {@inheritDoc} 470 | */ 471 | @Override 472 | public boolean containsValue(Object value) { 473 | if (root == null) { 474 | return false; 475 | } 476 | return root.containsValue(value); 477 | } 478 | 479 | /** 480 | * Add mapping. 481 | * 482 | * @param key The mapping's key. 483 | * @param value value The mapping's value 484 | * @throws NullPointerException if key or value is null. 485 | */ 486 | @Override 487 | public T put(CharSequence key, T value) throws NullPointerException { 488 | int len; 489 | T old; 490 | 491 | if (key == null) { 492 | throw new NullPointerException("Null keys are not handled"); 493 | } 494 | if (value == null) { 495 | throw new NullPointerException("Null values are not handled"); 496 | } 497 | if (root == null) { 498 | root = new Node<>(); 499 | } 500 | if ((old = root.put(key, 0, value)) != null) { 501 | return old; 502 | } 503 | 504 | // after in case of replacement 505 | if ((len = key.length()) > maxKeyLen) { 506 | maxKeyLen = len; 507 | } 508 | size++; 509 | return null; 510 | } 511 | 512 | /** 513 | * Remove a entry. 514 | * 515 | * @return previous value 516 | * @throws UnsupportedOperationException always. 517 | */ 518 | @Override 519 | public T remove(Object key) throws UnsupportedOperationException { 520 | throw new UnsupportedOperationException(); 521 | } 522 | 523 | /** 524 | * {@inheritDoc} 525 | */ 526 | @Override 527 | public void putAll(Map map) { 528 | for (Map.Entry entry : map.entrySet()) { 529 | put(entry.getKey(), entry.getValue()); 530 | } 531 | } 532 | 533 | /** 534 | * {@inheritDoc} 535 | */ 536 | @Override 537 | public Set keySet() { 538 | Set keys = new HashSet<>(size); 539 | 540 | if (root == null) { 541 | return keys; 542 | } 543 | return root.keySet(new StringBuilder(), keys); 544 | } 545 | 546 | /** 547 | * {@inheritDoc} 548 | */ 549 | @Override 550 | public Collection values() { 551 | ArrayList values = new ArrayList<>(size()); 552 | 553 | if (root == null) { 554 | return values; 555 | } 556 | return root.values(values); 557 | } 558 | 559 | /** 560 | * {@inheritDoc} 561 | */ 562 | @Override 563 | public Set> entrySet() { 564 | Set> entries = new HashSet<>(size()); 565 | 566 | if (root == null) { 567 | return entries; 568 | } 569 | return root.entrySet(new StringBuilder(), entries); 570 | } 571 | 572 | /** 573 | * Get the value for a key. 574 | * 575 | * @param key The key to look up. 576 | * @return The value for key or null if the key is not found. 577 | */ 578 | @Override 579 | public T get(Object key) { 580 | if (root == null || key == null) { 581 | return null; 582 | } 583 | if (!(key instanceof CharSequence)) { 584 | return null; 585 | } 586 | return root.get((CharSequence) key, 0); 587 | } 588 | 589 | /** 590 | * Get the number of entries. 591 | * 592 | * @return the number or entries. 593 | */ 594 | @Override 595 | public int size() { 596 | return size; 597 | } 598 | 599 | /** 600 | * {@inheritDoc} 601 | */ 602 | @SuppressWarnings("unchecked") 603 | @Override 604 | public boolean equals(Object other) { 605 | if (other == null) { 606 | return false; 607 | } 608 | if (!(other instanceof Map)) { 609 | return false; 610 | } 611 | // per spec 612 | return entrySet().equals(((Map) other).entrySet()); 613 | } 614 | 615 | /** 616 | * {@inheritDoc} 617 | */ 618 | @Override 619 | public int hashCode() { 620 | // per spec 621 | return entrySet().hashCode(); 622 | } 623 | 624 | /** 625 | * {@inheritDoc} 626 | */ 627 | @Override 628 | public String toString() { 629 | StringBuilder sb; 630 | boolean first; 631 | 632 | if (isEmpty()) { 633 | return "{}"; 634 | } 635 | sb = new StringBuilder(); 636 | first = true; 637 | sb.append("{ "); 638 | for (Map.Entry entry : entrySet()) { 639 | if (first) { 640 | first = false; 641 | } else { 642 | sb.append(", "); 643 | } 644 | sb.append(entry.toString()); 645 | } 646 | sb.append(" }"); 647 | return sb.toString(); 648 | } 649 | 650 | /** 651 | * {@inheritDoc} 652 | */ 653 | @Override 654 | public boolean isEmpty() { 655 | return (size() == 0); 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/PercentCodec.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.codecs; 14 | 15 | import java.io.UnsupportedEncodingException; 16 | import java.util.Set; 17 | 18 | import org.owasp.fileio.Encoder; 19 | 20 | /** 21 | * Implementation of the Codec interface for percent encoding (aka URL encoding). 22 | */ 23 | public class PercentCodec extends Codec { 24 | 25 | private static final Set UNENCODED_SET = Encoder.ALPHANUMERICS; 26 | 27 | /** 28 | * Convinence method to encode a string into UTF-8. This wraps the {@link UnsupportedEncodingException} that {@link String#getBytes(String)} throws in a {@link IllegalStateException} as UTF-8 29 | * support is required by the Java spec and should never throw this exception. 30 | * 31 | * @param str the string to encode 32 | * @return str encoded in UTF-8 as bytes. 33 | * @throws IllegalStateException wrapped {@link 34 | * UnsupportedEncodingException} if {@link String.getBytes(String)} throws it. 35 | */ 36 | private static byte[] toUtf8Bytes(String str) { 37 | try { 38 | return str.getBytes("UTF-8"); 39 | } catch (UnsupportedEncodingException e) { 40 | throw new IllegalStateException("The Java spec requires UTF-8 support.", e); 41 | } 42 | } 43 | 44 | /** 45 | * Append the two upper case hex characters for a byte. 46 | * 47 | * @param sb The string buffer to append to. 48 | * @param b The byte to hexify 49 | * @return sb with the hex characters appended. 50 | */ 51 | // rfc3986 2.1: For consistency, URI producers 52 | // should use uppercase hexadecimal digits for all percent- 53 | // encodings. 54 | private static StringBuilder appendTwoUpperHex(StringBuilder sb, int b) { 55 | if (b < Byte.MIN_VALUE || b > Byte.MAX_VALUE) { 56 | throw new IllegalArgumentException("b is not a byte (was " + b + ')'); 57 | } 58 | b &= 0xFF; 59 | if (b < 0x10) { 60 | sb.append('0'); 61 | } 62 | return sb.append(Integer.toHexString(b).toUpperCase()); 63 | } 64 | 65 | /** 66 | * Encode a character for URLs 67 | * 68 | * @param immune characters not to encode 69 | * @param c character to encode 70 | * @return the encoded string representing c 71 | */ 72 | public String encodeCharacter(char[] immune, Character c) { 73 | String cStr = String.valueOf(c.charValue()); 74 | byte[] bytes; 75 | StringBuilder sb; 76 | 77 | if (UNENCODED_SET.contains(c)) { 78 | return cStr; 79 | } 80 | 81 | bytes = toUtf8Bytes(cStr); 82 | sb = new StringBuilder(bytes.length * 3); 83 | for (byte b : bytes) { 84 | appendTwoUpperHex(sb.append('%'), b); 85 | } 86 | return sb.toString(); 87 | } 88 | 89 | /** 90 | * {@inheritDoc} 91 | * 92 | * Formats all are legal both upper/lower case: %hh; 93 | * 94 | * @param input encoded character using percent characters (such as URL encoding) 95 | */ 96 | public Character decodeCharacter(PushbackString input) { 97 | input.mark(); 98 | Character first = input.next(); 99 | if (first == null) { 100 | input.reset(); 101 | return null; 102 | } 103 | 104 | // if this is not an encoded character, return null 105 | if (first != '%') { 106 | input.reset(); 107 | return null; 108 | } 109 | 110 | // Search for exactly 2 hex digits following 111 | StringBuilder sb = new StringBuilder(); 112 | for (int i = 0; i < 2; i++) { 113 | Character c = input.nextHex(); 114 | if (c != null) { 115 | sb.append(c); 116 | } 117 | } 118 | if (sb.length() == 2) { 119 | try { 120 | // parse the hex digit and create a character 121 | int i = Integer.parseInt(sb.toString(), 16); 122 | if (Character.isValidCodePoint(i)) { 123 | return (char) i; 124 | } 125 | } catch (NumberFormatException ignored) { 126 | } 127 | } 128 | input.reset(); 129 | return null; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/PushbackString.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.codecs; 14 | 15 | /** 16 | * The pushback string is used by Codecs to allow them to push decoded characters back onto a string for further decoding. This is necessary to detect double-encoding. 17 | */ 18 | public class PushbackString { 19 | 20 | private String input; 21 | private Character pushback; 22 | private Character temp; 23 | private int index = 0; 24 | private int mark = 0; 25 | 26 | /** 27 | * Constructs a new instance of PushbackString 28 | * @param input the String to decode 29 | */ 30 | public PushbackString(String input) { 31 | this.input = input; 32 | } 33 | 34 | /** 35 | * 36 | * @param c The character to set as the pushback 37 | */ 38 | public void pushback(Character c) { 39 | pushback = c; 40 | } 41 | 42 | /** 43 | * Get the current index of the PushbackString. Typically used in error messages. 44 | * 45 | * @return The current index of the PushbackString. 46 | */ 47 | public int index() { 48 | return index; 49 | } 50 | 51 | /** 52 | * 53 | * @return true if there are more characters to process 54 | */ 55 | public boolean hasNext() { 56 | if (pushback != null) { 57 | return true; 58 | } 59 | if (input == null) { 60 | return false; 61 | } 62 | if (input.length() == 0) { 63 | return false; 64 | } 65 | if (index >= input.length()) { 66 | return false; 67 | } 68 | return true; 69 | } 70 | 71 | /** 72 | * 73 | * @return the next character 74 | */ 75 | public Character next() { 76 | if (pushback != null) { 77 | Character save = pushback; 78 | pushback = null; 79 | return save; 80 | } 81 | if (input == null) { 82 | return null; 83 | } 84 | if (input.length() == 0) { 85 | return null; 86 | } 87 | if (index >= input.length()) { 88 | return null; 89 | } 90 | return Character.valueOf(input.charAt(index++)); 91 | } 92 | 93 | /** 94 | * 95 | * @return the next hex digit in the input, or null if the next character is not hex 96 | */ 97 | public Character nextHex() { 98 | Character c = next(); 99 | if (c == null) { 100 | return null; 101 | } 102 | if (isHexDigit(c)) { 103 | return c; 104 | } 105 | return null; 106 | } 107 | 108 | /** 109 | * 110 | * @return the next octal digit in the input, or null if the next character is not octal 111 | */ 112 | public Character nextOctal() { 113 | Character c = next(); 114 | if (c == null) { 115 | return null; 116 | } 117 | if (isOctalDigit(c)) { 118 | return c; 119 | } 120 | return null; 121 | } 122 | 123 | /** 124 | * Returns true if the parameter character is a hexidecimal digit 0 through 9, a through f, or A through F. 125 | * 126 | * @param c The Character to test 127 | * @return true if the input character is a hex digit (0-F) 128 | */ 129 | public static boolean isHexDigit(Character c) { 130 | if (c == null) { 131 | return false; 132 | } 133 | char ch = c.charValue(); 134 | return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); 135 | } 136 | 137 | /** 138 | * Returns true if the parameter character is an octal digit 0 through 7. 139 | * 140 | * @param c The Character to test 141 | * @return true if the input character is an octal digit (0-7) 142 | */ 143 | public static boolean isOctalDigit(Character c) { 144 | if (c == null) { 145 | return false; 146 | } 147 | char ch = c.charValue(); 148 | return ch >= '0' && ch <= '7'; 149 | } 150 | 151 | /** 152 | * Return the next character without affecting the current index. 153 | * 154 | * @return the next Character in the input 155 | */ 156 | public Character peek() { 157 | if (pushback != null) { 158 | return pushback; 159 | } 160 | if (input == null) { 161 | return null; 162 | } 163 | if (input.length() == 0) { 164 | return null; 165 | } 166 | if (index >= input.length()) { 167 | return null; 168 | } 169 | return Character.valueOf(input.charAt(index)); 170 | } 171 | 172 | /** 173 | * Test to see if the next character is a particular value without affecting the current index. 174 | * 175 | * @param c The character to test for 176 | * @return true if the next character matches the input character 177 | */ 178 | public boolean peek(char c) { 179 | if (pushback != null && pushback.charValue() == c) { 180 | return true; 181 | } 182 | if (input == null) { 183 | return false; 184 | } 185 | if (input.length() == 0) { 186 | return false; 187 | } 188 | if (index >= input.length()) { 189 | return false; 190 | } 191 | return input.charAt(index) == c; 192 | } 193 | 194 | /** 195 | * 196 | */ 197 | public void mark() { 198 | temp = pushback; 199 | mark = index; 200 | } 201 | 202 | /** 203 | * 204 | */ 205 | public void reset() { 206 | pushback = temp; 207 | index = mark; 208 | } 209 | 210 | /** 211 | * 212 | * @return the remainder of the input String, prepended with any pushback, if necessary 213 | */ 214 | protected String remainder() { 215 | String output = input.substring(index); 216 | if (pushback != null) { 217 | output = pushback + output; 218 | } 219 | return output; 220 | } 221 | } -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/codecs/Trie.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 10 | * @created 2014 11 | */ 12 | package org.owasp.fileio.codecs; 13 | 14 | import java.io.IOException; 15 | import java.io.PushbackReader; 16 | import java.util.Collection; 17 | import java.util.Collections; 18 | import java.util.Map; 19 | import java.util.Set; 20 | 21 | public interface Trie extends Map { 22 | 23 | public Map.Entry getLongestMatch(CharSequence key); 24 | 25 | public Map.Entry getLongestMatch(PushbackReader keyIn) throws IOException; 26 | 27 | public int getMaxKeyLength(); 28 | 29 | static class TrieProxy implements Trie { 30 | 31 | private Trie wrapped; 32 | 33 | TrieProxy(Trie toWrap) { 34 | wrapped = toWrap; 35 | } 36 | 37 | protected Trie getWrapped() { 38 | return wrapped; 39 | } 40 | 41 | public Map.Entry getLongestMatch(CharSequence key) { 42 | return wrapped.getLongestMatch(key); 43 | } 44 | 45 | public Map.Entry getLongestMatch(PushbackReader keyIn) throws IOException { 46 | return wrapped.getLongestMatch(keyIn); 47 | } 48 | 49 | public int getMaxKeyLength() { 50 | return wrapped.getMaxKeyLength(); 51 | } 52 | 53 | /* java.util.Map: */ 54 | public int size() { 55 | return wrapped.size(); 56 | } 57 | 58 | public boolean isEmpty() { 59 | return wrapped.isEmpty(); 60 | } 61 | 62 | public boolean containsKey(Object key) { 63 | return wrapped.containsKey(key); 64 | } 65 | 66 | public boolean containsValue(Object val) { 67 | return wrapped.containsValue(val); 68 | } 69 | 70 | public T get(Object key) { 71 | return wrapped.get(key); 72 | } 73 | 74 | public T put(CharSequence key, T value) { 75 | return wrapped.put(key, value); 76 | } 77 | 78 | public T remove(Object key) { 79 | return wrapped.remove(key); 80 | } 81 | 82 | public void putAll(Map t) { 83 | wrapped.putAll(t); 84 | } 85 | 86 | public void clear() { 87 | wrapped.clear(); 88 | } 89 | 90 | public Set keySet() { 91 | return wrapped.keySet(); 92 | } 93 | 94 | public Collection values() { 95 | return wrapped.values(); 96 | } 97 | 98 | public Set> entrySet() { 99 | return wrapped.entrySet(); 100 | } 101 | 102 | public boolean equals(Object other) { 103 | return wrapped.equals(other); 104 | } 105 | 106 | public int hashCode() { 107 | return wrapped.hashCode(); 108 | } 109 | } 110 | 111 | static class Unmodifiable extends TrieProxy { 112 | 113 | Unmodifiable(Trie toWrap) { 114 | super(toWrap); 115 | } 116 | 117 | public T put(CharSequence key, T value) { 118 | throw new UnsupportedOperationException("Unmodifiable Trie"); 119 | } 120 | 121 | public T remove(CharSequence key) { 122 | throw new UnsupportedOperationException("Unmodifiable Trie"); 123 | } 124 | 125 | public void putAll(Map t) { 126 | throw new UnsupportedOperationException("Unmodifiable Trie"); 127 | } 128 | 129 | public void clear() { 130 | throw new UnsupportedOperationException("Unmodifiable Trie"); 131 | } 132 | 133 | public Set keySet() { 134 | return Collections.unmodifiableSet(super.keySet()); 135 | } 136 | 137 | public Collection values() { 138 | return Collections.unmodifiableCollection(super.values()); 139 | } 140 | 141 | public Set> entrySet() { 142 | return Collections.unmodifiableSet(super.entrySet()); 143 | } 144 | } 145 | 146 | public static class Util { 147 | 148 | private Util() { 149 | } 150 | 151 | static Trie unmodifiable(Trie toWrap) { 152 | return new Unmodifiable(toWrap); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/util/CollectionsUtil.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Neil Matatall (neil.matatall .at. gmail.com) - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.util; 14 | 15 | import java.util.Collections; 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | /** 20 | * Are these necessary? Are there any libraries or java.lang classes to take care of the conversions? 21 | * 22 | * FIXME: we can convert to using this, but it requires that the array be of Character, not char new HashSet(Arrays.asList(array)) 23 | */ 24 | public class CollectionsUtil { 25 | 26 | private static final char[] EMPTY_CHAR_ARRAY = new char[0]; 27 | 28 | /** 29 | * Converts an array of chars to a Set of Characters. 30 | * 31 | * @param array the contents of the new Set 32 | * @return a Set containing the elements in the array 33 | */ 34 | public static Set arrayToSet(char... array) { 35 | Set toReturn; 36 | 37 | if (array == null) { 38 | return new HashSet(); 39 | } 40 | toReturn = new HashSet(array.length); 41 | for (char c : array) { 42 | toReturn.add(c); 43 | } 44 | return toReturn; 45 | } 46 | 47 | /** 48 | * Convert a char array to a unmodifiable Set. 49 | * 50 | * @param array the contents of the new Set 51 | * @return a unmodifiable Set containing the elements in the array. 52 | */ 53 | public static Set arrayToUnmodifiableSet(char... array) { 54 | if (array == null) { 55 | return Collections.emptySet(); 56 | } 57 | if (array.length == 1) { 58 | return Collections.singleton(array[0]); 59 | } 60 | return Collections.unmodifiableSet(arrayToSet(array)); 61 | } 62 | 63 | /** 64 | * Convert a String to a char array 65 | * 66 | * @param str The string to convert 67 | * @return character array containing the characters in str. An empty array is returned if str is null. 68 | */ 69 | public static char[] strToChars(String str) { 70 | int len; 71 | char[] ret; 72 | 73 | if (str == null) { 74 | return EMPTY_CHAR_ARRAY; 75 | } 76 | len = str.length(); 77 | ret = new char[len]; 78 | str.getChars(0, len, ret, 0); 79 | return ret; 80 | } 81 | 82 | /** 83 | * Convert a String to a set of characters. 84 | * 85 | * @param str The string to convert 86 | * @return A set containing the characters in str. A empty set is returned if str is null. 87 | */ 88 | public static Set strToSet(String str) { 89 | Set set; 90 | 91 | if (str == null) { 92 | return new HashSet(); 93 | } 94 | set = new HashSet(str.length()); 95 | for (int i = 0; i < str.length(); i++) { 96 | set.add(str.charAt(i)); 97 | } 98 | return set; 99 | } 100 | 101 | /** 102 | * Convert a String to a unmodifiable set of characters. 103 | * 104 | * @param str The string to convert 105 | * @return A set containing the characters in str. A empty set is returned if str is null. 106 | */ 107 | public static Set strToUnmodifiableSet(String str) { 108 | if (str == null) { 109 | return Collections.emptySet(); 110 | } 111 | if (str.length() == 1) { 112 | return Collections.singleton(str.charAt(0)); 113 | } 114 | return Collections.unmodifiableSet(strToSet(str)); 115 | } 116 | 117 | /** 118 | * Private constructor to prevent instantiation. 119 | */ 120 | private CollectionsUtil() { 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/util/NullSafe.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 10 | * @created 2014 11 | */ 12 | package org.owasp.fileio.util; 13 | 14 | public class NullSafe { 15 | 16 | /** 17 | * Class should not be instantiated. 18 | */ 19 | private NullSafe() { 20 | } 21 | 22 | /** 23 | * {@link Object#equals(Object)} that safely handles nulls. 24 | * 25 | * @param a First object 26 | * @param b Second object 27 | * @return true if a == b or a.equals(b). false otherwise. 28 | */ 29 | public static boolean equals(Object a, Object b) { 30 | if (a == b) // short cut same object 31 | { 32 | return true; 33 | } 34 | if (a == null) { 35 | return (b == null); 36 | } 37 | if (b == null) { 38 | return false; 39 | } 40 | return a.equals(b); 41 | } 42 | 43 | /** 44 | * {@link Object#hashCode()} of an object. 45 | * 46 | * @param o Object to get a hashCode for. 47 | * @return 0 if o is null. Otherwise o.hashCode(). 48 | */ 49 | public static int hashCode(Object o) { 50 | if (o == null) { 51 | return 0; 52 | } 53 | return o.hashCode(); 54 | } 55 | 56 | /** 57 | * {@link Object#toString()} of an object. 58 | * 59 | * @param o Object to get a String for. 60 | * @return "(null)" o is null. Otherwise o.toString(). 61 | */ 62 | public static String toString(Object o) { 63 | if (o == null) { 64 | return "(null)"; 65 | } 66 | return o.toString(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/org/owasp/fileio/util/Utils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.util; 14 | 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | /** 19 | * This class provides a number of utility functions. 20 | * 21 | * @author August Detlefsen [augustd at codemagi dot com] 22 | */ 23 | public class Utils { 24 | 25 | /** 26 | * Converts an array of chars to a Set of Characters. 27 | * 28 | * @param array the contents of the new Set 29 | * @return a Set containing the elements in the array 30 | */ 31 | public static Set arrayToSet(char... array) { 32 | Set toReturn; 33 | if (array == null) { 34 | return new HashSet(); 35 | } 36 | toReturn = new HashSet(array.length); 37 | for (char c : array) { 38 | toReturn.add(c); 39 | } 40 | return toReturn; 41 | } 42 | 43 | /** 44 | * Helper function to check if a String is empty 45 | * 46 | * @param input string input value 47 | * @return boolean response if input is empty or not 48 | */ 49 | public static boolean isEmpty(String input) { 50 | return input == null || input.trim().length() == 0; 51 | } 52 | 53 | /** 54 | * Helper function to check if a byte array is empty 55 | * 56 | * @param input string input value 57 | * @return boolean response if input is empty or not 58 | */ 59 | public static boolean isEmpty(byte[] input) { 60 | return (input == null || input.length == 0); 61 | } 62 | 63 | /** 64 | * Helper function to check if a char array is empty 65 | * 66 | * @param input string input value 67 | * @return boolean response if input is empty or not 68 | */ 69 | public static boolean isEmpty(char[] input) { 70 | return (input == null || input.length == 0); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/org/owasp/fileio/FileValidatorTest.java: -------------------------------------------------------------------------------- 1 | package org.owasp.fileio; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.UnsupportedEncodingException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import static org.junit.Assert.*; 9 | 10 | import org.junit.Test; 11 | import org.owasp.fileio.codecs.Codec; 12 | import org.owasp.fileio.codecs.HTMLEntityCodec; 13 | 14 | /** 15 | * 16 | * @author August Detlefsen 17 | */ 18 | public class FileValidatorTest { 19 | 20 | private static final String PREFERRED_ENCODING = "UTF-8"; 21 | 22 | @Test 23 | public void testIsValidFileName() { 24 | System.out.println("isValidFileName"); 25 | FileValidator instance = new FileValidator(); 26 | assertTrue("Simple valid filename with a valid extension", instance.isValidFileName("test", "aspect.jar", false)); 27 | assertTrue("All valid filename characters are accepted", instance.isValidFileName("test", "!@#$%^&{}[]()_+-=,.~'` abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.jar", false)); 28 | assertTrue("Legal filenames that decode to legal filenames are accepted", instance.isValidFileName("test", "aspe%20ct.jar", false)); 29 | 30 | List errors = new ArrayList<>(); 31 | assertTrue("Simple valid filename with a valid extension", instance.isValidFileName("test", "aspect.jar", false, errors)); 32 | assertTrue("All valid filename characters are accepted", instance.isValidFileName("test", "!@#$%^&{}[]()_+-=,.~'` abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890.jar", false, errors)); 33 | assertTrue("Legal filenames that decode to legal filenames are accepted", instance.isValidFileName("test", "aspe%20ct.jar", false, errors)); 34 | assertTrue(errors.isEmpty()); 35 | } 36 | 37 | @Test 38 | public void testIsValidFileUpload() throws IOException { 39 | System.out.println("isValidFileUpload"); 40 | String filepath = new File(System.getProperty("user.dir")).getCanonicalPath(); 41 | String filename = "aspect.jar"; 42 | File parent = new File("/").getCanonicalFile(); 43 | List errors = new ArrayList<>(); 44 | byte[] content = null; 45 | try { 46 | content = "This is some file content".getBytes(PREFERRED_ENCODING); 47 | } catch (UnsupportedEncodingException e) { 48 | fail(PREFERRED_ENCODING + " not a supported encoding?!?!!!"); 49 | } 50 | FileValidator instance = new FileValidator(); 51 | try { 52 | assertTrue(instance.isValidFileUpload("test", filepath, filename, parent, content, 100, false)); 53 | } catch (ValidationException ve) { 54 | //no-op. We want to know about errors! 55 | } 56 | assertTrue(instance.isValidFileUpload("test", filepath, filename, parent, content, 100, false, errors)); 57 | assertTrue(errors.size() == 0); 58 | 59 | filepath = "/ridiculous"; 60 | filename = "aspect.jar"; 61 | try { 62 | content = "This is some file content".getBytes(PREFERRED_ENCODING); 63 | } catch (UnsupportedEncodingException e) { 64 | fail(PREFERRED_ENCODING + " not a supported encoding?!?!!!"); 65 | } 66 | try { 67 | assertFalse(instance.isValidFileUpload("test", filepath, filename, parent, content, 100, false)); 68 | } catch (ValidationException ve) { 69 | //no-op. We want to know about errors! 70 | } 71 | assertFalse(instance.isValidFileUpload("test", filepath, filename, parent, content, 100, false, errors)); 72 | assertTrue(errors.size() == 1); 73 | } 74 | 75 | @Test 76 | public void testIsInvalidFilename() { 77 | System.out.println("testIsInvalidFilename"); 78 | FileValidator instance = new FileValidator(); 79 | char invalidChars[] = "/\\:*?\"<>|".toCharArray(); 80 | for (int i = 0; i < invalidChars.length; i++) { 81 | assertFalse(invalidChars[i] + " is an invalid character for a filename", 82 | instance.isValidFileName("test", "ow" + invalidChars[i] + "asp.jar", false)); 83 | } 84 | assertFalse("Files must have an extension", instance.isValidFileName("test", "", false)); 85 | assertFalse("Files must have a valid extension", instance.isValidFileName("test.invalidExtension", "", false)); 86 | assertFalse("Filennames cannot be the empty string", instance.isValidFileName("test", "", false)); 87 | } 88 | 89 | @Test 90 | public void testIsValidDirectoryPath() throws IOException { 91 | System.out.println("isValidDirectoryPath"); 92 | 93 | // get an encoder with a special list of codecs and make a validator out of it 94 | List list = new ArrayList(); 95 | list.add(new HTMLEntityCodec()); 96 | Encoder encoder = new Encoder(list); 97 | FileValidator instance = new FileValidator(encoder); 98 | 99 | boolean isWindows = (System.getProperty("os.name").indexOf("Windows") != -1) ? true : false; 100 | File parent = new File("/"); 101 | 102 | List errors = new ArrayList<>(); 103 | 104 | if (isWindows) { 105 | String sysRoot = new File(System.getenv("SystemRoot")).getCanonicalPath(); 106 | // Windows paths that don't exist and thus should fail 107 | assertFalse(instance.isValidDirectoryPath("test", "c:\\ridiculous", parent, false)); 108 | assertFalse(instance.isValidDirectoryPath("test", "c:\\jeff", parent, false)); 109 | assertFalse(instance.isValidDirectoryPath("test", "c:\\temp\\..\\etc", parent, false)); 110 | 111 | // Windows paths 112 | assertTrue(instance.isValidDirectoryPath("test", "C:\\", parent, false)); // Windows root directory 113 | assertTrue(instance.isValidDirectoryPath("test", sysRoot, parent, false)); // Windows always exist directory 114 | assertFalse(instance.isValidDirectoryPath("test", sysRoot + "\\System32\\cmd.exe", parent, false)); // Windows command shell 115 | 116 | // Unix specific paths should not pass 117 | assertFalse(instance.isValidDirectoryPath("test", "/tmp", parent, false)); // Unix Temporary directory 118 | assertFalse(instance.isValidDirectoryPath("test", "/bin/sh", parent, false)); // Unix Standard shell 119 | assertFalse(instance.isValidDirectoryPath("test", "/etc/config", parent, false)); 120 | 121 | // Unix specific paths that should not exist or work 122 | assertFalse(instance.isValidDirectoryPath("test", "/etc/ridiculous", parent, false)); 123 | assertFalse(instance.isValidDirectoryPath("test", "/tmp/../etc", parent, false)); 124 | 125 | assertFalse(instance.isValidDirectoryPath("test1", "c:\\ridiculous", parent, false, errors)); 126 | assertTrue(errors.size() == 1); 127 | assertFalse(instance.isValidDirectoryPath("test2", "c:\\jeff", parent, false, errors)); 128 | assertTrue(errors.size() == 2); 129 | assertFalse(instance.isValidDirectoryPath("test3", "c:\\temp\\..\\etc", parent, false, errors)); 130 | assertTrue(errors.size() == 3); 131 | 132 | // Windows paths 133 | assertTrue(instance.isValidDirectoryPath("test4", "C:\\", parent, false, errors)); // Windows root directory 134 | assertTrue(errors.size() == 3); 135 | assertTrue(instance.isValidDirectoryPath("test5", sysRoot, parent, false, errors)); // Windows always exist directory 136 | assertTrue(errors.size() == 3); 137 | assertFalse(instance.isValidDirectoryPath("test6", sysRoot + "\\System32\\cmd.exe", parent, false, errors)); // Windows command shell 138 | assertTrue(errors.size() == 4); 139 | 140 | // Unix specific paths should not pass 141 | assertFalse(instance.isValidDirectoryPath("test7", "/tmp", parent, false, errors)); // Unix Temporary directory 142 | assertTrue(errors.size() == 5); 143 | assertFalse(instance.isValidDirectoryPath("test8", "/bin/sh", parent, false, errors)); // Unix Standard shell 144 | assertTrue(errors.size() == 6); 145 | assertFalse(instance.isValidDirectoryPath("test9", "/etc/config", parent, false, errors)); 146 | assertTrue(errors.size() == 7); 147 | 148 | // Unix specific paths that should not exist or work 149 | assertFalse(instance.isValidDirectoryPath("test10", "/etc/ridiculous", parent, false, errors)); 150 | assertTrue(errors.size() == 8); 151 | assertFalse(instance.isValidDirectoryPath("test11", "/tmp/../etc", parent, false, errors)); 152 | assertTrue(errors.size() == 9); 153 | 154 | } else { 155 | // Windows paths should fail 156 | assertFalse(instance.isValidDirectoryPath("test", "c:\\ridiculous", parent, false)); 157 | assertFalse(instance.isValidDirectoryPath("test", "c:\\temp\\..\\etc", parent, false)); 158 | 159 | // Standard Windows locations should fail 160 | assertFalse(instance.isValidDirectoryPath("test", "c:\\", parent, false)); // Windows root directory 161 | assertFalse(instance.isValidDirectoryPath("test", "c:\\Windows\\temp", parent, false)); // Windows temporary directory 162 | assertFalse(instance.isValidDirectoryPath("test", "c:\\Windows\\System32\\cmd.exe", parent, false)); // Windows command shell 163 | 164 | // Unix specific paths should pass 165 | assertTrue(instance.isValidDirectoryPath("test", "/", parent, false)); // Root directory 166 | assertTrue(instance.isValidDirectoryPath("test", "/bin", parent, false)); // Always exist directory 167 | 168 | // Unix specific paths that should not exist or work 169 | assertFalse(instance.isValidDirectoryPath("test", "/bin/sh", parent, false)); // Standard shell, not dir 170 | assertFalse(instance.isValidDirectoryPath("test", "/etc/ridiculous", parent, false)); 171 | assertFalse(instance.isValidDirectoryPath("test", "/tmp/../etc", parent, false)); 172 | 173 | // Windows paths should fail 174 | assertFalse(instance.isValidDirectoryPath("test1", "c:\\ridiculous", parent, false, errors)); 175 | assertTrue(errors.size() == 1); 176 | assertFalse(instance.isValidDirectoryPath("test2", "c:\\temp\\..\\etc", parent, false, errors)); 177 | assertTrue(errors.size() == 2); 178 | 179 | // Standard Windows locations should fail 180 | assertFalse(instance.isValidDirectoryPath("test3", "c:\\", parent, false, errors)); // Windows root directory 181 | assertTrue(errors.size() == 3); 182 | assertFalse(instance.isValidDirectoryPath("test4", "c:\\Windows\\temp", parent, false, errors)); // Windows temporary directory 183 | assertTrue(errors.size() == 4); 184 | assertFalse(instance.isValidDirectoryPath("test5", "c:\\Windows\\System32\\cmd.exe", parent, false, errors)); // Windows command shell 185 | assertTrue(errors.size() == 5); 186 | 187 | // Unix specific paths should pass 188 | assertTrue(instance.isValidDirectoryPath("test6", "/", parent, false, errors)); // Root directory 189 | assertTrue(errors.size() == 5); 190 | assertTrue(instance.isValidDirectoryPath("test7", "/bin", parent, false, errors)); // Always exist directory 191 | assertTrue(errors.size() == 5); 192 | 193 | // Unix specific paths that should not exist or work 194 | assertFalse(instance.isValidDirectoryPath("test8", "/bin/sh", parent, false, errors)); // Standard shell, not dir 195 | assertTrue(errors.size() == 6); 196 | assertFalse(instance.isValidDirectoryPath("test9", "/etc/ridiculous", parent, false, errors)); 197 | assertTrue(errors.size() == 7); 198 | assertFalse(instance.isValidDirectoryPath("test10", "/tmp/../etc", parent, false, errors)); 199 | assertTrue(errors.size() == 8); 200 | } 201 | } 202 | 203 | @Test 204 | public void TestIsValidDirectoryPath() { 205 | // isValidDirectoryPath(String, String, boolean) 206 | } 207 | } -------------------------------------------------------------------------------- /src/test/java/org/owasp/fileio/SafeFileTest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio; 14 | 15 | import org.owasp.fileio.util.FileTestUtils; 16 | import static org.junit.Assert.*; 17 | 18 | import java.io.File; 19 | import java.util.Iterator; 20 | import java.util.Set; 21 | 22 | import org.junit.After; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.owasp.fileio.util.CollectionsUtil; 26 | 27 | public class SafeFileTest { 28 | 29 | private static final Class CLASS = SafeFileTest.class; 30 | private static final String CLASS_NAME = CLASS.getName(); 31 | 32 | /** 33 | * Name of the file in the temporary directory 34 | */ 35 | private static final String TEST_FILE_NAME = "test.file"; 36 | private static final Set GOOD_FILE_CHARS = CollectionsUtil.strToUnmodifiableSet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" /* + "." */); 37 | private static final Set BAD_FILE_CHARS = CollectionsUtil.strToUnmodifiableSet("\u0000" + /*(File.separatorChar == '/' ? '\\' : '/') +*/ "*|<>?:" /*+ "~!@#$%^&(){}[],`;"*/); 38 | private File testDir = null; 39 | private File testFile = null; 40 | String pathWithNullByte = "/temp/file.txt" + (char) 0; 41 | 42 | @Before 43 | public void setUp() throws Exception { 44 | // create a file to test with 45 | testDir = FileTestUtils.createTmpDirectory(CLASS_NAME).getCanonicalFile(); 46 | testFile = new File(testDir, TEST_FILE_NAME); 47 | testFile.createNewFile(); 48 | testFile = testFile.getCanonicalFile(); 49 | } 50 | 51 | @After 52 | public void tearDown() throws Exception { 53 | FileTestUtils.deleteRecursively(testDir); 54 | } 55 | 56 | @Test 57 | public void testEscapeCharactersInFilename() { 58 | System.out.println("testEscapeCharactersInFilenameInjection"); 59 | File tf = testFile; 60 | if (tf.exists()) { 61 | System.out.println("File is there: " + tf); 62 | } 63 | 64 | File sf = new File(testDir, "test^.file"); 65 | if (sf.exists()) { 66 | System.out.println(" Injection allowed " + sf.getAbsolutePath()); 67 | } else { 68 | System.out.println(" Injection didn't work " + sf.getAbsolutePath()); 69 | } 70 | } 71 | 72 | @Test 73 | public void testEscapeCharacterInDirectoryInjection() { 74 | System.out.println("testEscapeCharacterInDirectoryInjection"); 75 | File sf = new File(testDir, "test\\^.^.\\file"); 76 | if (sf.exists()) { 77 | System.out.println(" Injection allowed " + sf.getAbsolutePath()); 78 | } else { 79 | System.out.println(" Injection didn't work " + sf.getAbsolutePath()); 80 | } 81 | } 82 | 83 | @Test 84 | public void testJavaFileInjectionGood() throws ValidationException { 85 | for (Iterator i = GOOD_FILE_CHARS.iterator(); i.hasNext();) { 86 | String ch = i.next().toString(); // avoids generic issues in 1.4&1.5 87 | File sf = new SafeFile(testDir, TEST_FILE_NAME + ch); 88 | assertFalse("File \"" + TEST_FILE_NAME + ch + "\" should not exist ((int)ch=" + (int) ch.charAt(0) + ").", sf.exists()); 89 | sf = new SafeFile(testDir, TEST_FILE_NAME + ch + "test"); 90 | assertFalse("File \"" + TEST_FILE_NAME + ch + "\" should not exist ((int)ch=" + (int) ch.charAt(0) + ").", sf.exists()); 91 | } 92 | } 93 | 94 | @Test 95 | public void testJavaFileInjectionBad() { 96 | for (Iterator i = BAD_FILE_CHARS.iterator(); i.hasNext();) { 97 | String ch = i.next().toString(); // avoids generic issues in 1.4&1.5 98 | try { 99 | new SafeFile(testDir, TEST_FILE_NAME + ch); 100 | fail("Able to create SafeFile \"" + TEST_FILE_NAME + ch + "\" ((int)ch=" + (int) ch.charAt(0) + ")."); 101 | } catch (ValidationException expected) { 102 | } 103 | try { 104 | new SafeFile(testDir, TEST_FILE_NAME + ch + "test"); 105 | fail("Able to create SafeFile \"" + TEST_FILE_NAME + ch + "\" ((int)ch=" + (int) ch.charAt(0) + ")."); 106 | } catch (ValidationException expected) { 107 | } 108 | } 109 | } 110 | 111 | @Test 112 | public void testMultipleJavaFileInjectionGood() throws ValidationException { 113 | for (Iterator i = GOOD_FILE_CHARS.iterator(); i.hasNext();) { 114 | String ch = i.next().toString(); // avoids generic issues in 1.4&1.5 115 | ch = ch + ch + ch; 116 | File sf = new SafeFile(testDir, TEST_FILE_NAME + ch); 117 | assertFalse("File \"" + TEST_FILE_NAME + ch + "\" should not exist ((int)ch=" + (int) ch.charAt(0) + ").", sf.exists()); 118 | sf = new SafeFile(testDir, TEST_FILE_NAME + ch + "test"); 119 | assertFalse("File \"" + TEST_FILE_NAME + ch + "\" should not exist ((int)ch=" + (int) ch.charAt(0) + ").", sf.exists()); 120 | } 121 | } 122 | 123 | @Test 124 | public void testMultipleJavaFileInjectionBad() { 125 | for (Iterator i = BAD_FILE_CHARS.iterator(); i.hasNext();) { 126 | String ch = i.next().toString(); // avoids generic issues in 1.4&1.5 127 | ch = ch + ch + ch; 128 | try { 129 | new SafeFile(testDir, TEST_FILE_NAME + ch); 130 | fail("Able to create SafeFile \"" + TEST_FILE_NAME + ch + "\" ((int)ch=" + (int) ch.charAt(0) + ")."); 131 | } catch (ValidationException expected) { 132 | } 133 | try { 134 | new SafeFile(testDir, TEST_FILE_NAME + ch + "test"); 135 | fail("Able to create SafeFile \"" + TEST_FILE_NAME + ch + "\" ((int)ch=" + (int) ch.charAt(0) + ")."); 136 | } catch (ValidationException expected) { 137 | } 138 | } 139 | } 140 | 141 | @Test 142 | public void testAlternateDataStream() { 143 | try { 144 | File sf = new SafeFile(testDir, TEST_FILE_NAME + ":secret.txt"); 145 | fail("Able to construct SafeFile for alternate data stream: " + sf.getPath()); 146 | } catch (ValidationException expected) { 147 | } 148 | } 149 | 150 | @Test 151 | public void testSafeFileWithoutPath() { 152 | try { 153 | new SafeFile("hello.txt"); 154 | } catch (ValidationException expected) { 155 | } 156 | } 157 | 158 | static public String toHex(final byte b) { 159 | final char hexDigit[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; 160 | final char[] array = {hexDigit[(b >> 4) & 0x0f], hexDigit[b & 0x0f]}; 161 | return new String(array); 162 | } 163 | 164 | @Test 165 | public void testCreatePath() throws Exception { 166 | SafeFile sf = new SafeFile(testFile.getPath()); 167 | assertTrue(sf.exists()); 168 | } 169 | 170 | @Test 171 | public void testCreateParentPathName() throws Exception { 172 | SafeFile sf = new SafeFile(testDir, testFile.getName()); 173 | assertTrue(sf.exists()); 174 | } 175 | 176 | @Test 177 | public void testCreateParentFileName() throws Exception { 178 | SafeFile sf = new SafeFile(testFile.getParentFile(), testFile.getName()); 179 | assertTrue(sf.exists()); 180 | } 181 | 182 | @Test 183 | public void testCreateURI() throws Exception { 184 | SafeFile sf = new SafeFile(testFile.toURI()); 185 | assertTrue(sf.exists()); 186 | } 187 | 188 | @Test 189 | public void testCreateFileNamePercentNull() { 190 | try { 191 | new SafeFile(testDir + File.separator + "file%00.txt"); 192 | fail("no exception thrown for file name with percent encoded null"); 193 | } catch (ValidationException expected) { 194 | } 195 | } 196 | 197 | @Test 198 | public void testCreateFileNameQuestion() { 199 | try { 200 | new SafeFile(testFile.getParent() + File.separator + "file?.txt"); 201 | fail("no exception thrown for file name with question mark in it"); 202 | } catch (ValidationException e) { 203 | // expected 204 | } 205 | } 206 | 207 | @Test 208 | public void testCreateFileNameNull() { 209 | try { 210 | new SafeFile(testFile.getParent() + File.separator + "file" + ((char) 0) + ".txt"); 211 | fail("no exception thrown for file name with null in it"); 212 | } catch (ValidationException e) { 213 | // expected 214 | } 215 | } 216 | 217 | @Test 218 | public void testCreateFileHighByte() { 219 | try { 220 | new SafeFile(testFile.getParent() + File.separator + "file" + ((char) 160) + ".txt"); 221 | fail("no exception thrown for file name with high byte in it"); 222 | } catch (ValidationException e) { 223 | // expected 224 | } 225 | } 226 | 227 | @Test 228 | public void testCreateParentPercentNull() { 229 | try { 230 | new SafeFile(testFile.getParent() + File.separator + "file%00.txt"); 231 | fail("no exception thrown for file name with percent encoded null"); 232 | } catch (ValidationException e) { 233 | // expected 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/test/java/org/owasp/fileio/util/FileTestUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of the Open Web Application Security Project (OWASP) Java File IO Security project. For details, please see 3 | * https://www.owasp.org/index.php/OWASP_Java_File_I_O_Security_Project. 4 | * 5 | * Copyright (c) 2014 - The OWASP Foundation 6 | * 7 | * This API is published by OWASP under the Apache 2.0 license. You should read and accept the LICENSE before you use, modify, and/or redistribute this software. 8 | * 9 | * @author Jeff Williams Aspect Security - Original ESAPI author 10 | * @author August Detlefsen CodeMagi - Java File IO Security Project lead 11 | * @created 2014 12 | */ 13 | package org.owasp.fileio.util; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.security.SecureRandom; 18 | import java.util.Random; 19 | 20 | /** 21 | * Utilities to help with tests that involve files or directories. 22 | */ 23 | public class FileTestUtils { 24 | 25 | private static final Class CLASS = FileTestUtils.class; 26 | private static final String CLASS_NAME = CLASS.getName(); 27 | private static final String DEFAULT_PREFIX = CLASS_NAME + '.'; 28 | private static final String DEFAULT_SUFFIX = ".tmp"; 29 | private static final Random rand; 30 | 31 | /* 32 | Rational for switching from SecureRandom to Random: 33 | 34 | This is used for generating filenames for temporary 35 | directories. Origionally this was using SecureRandom for 36 | this to make /tmp races harder. This is not necessary as 37 | mkdir always returns false if if the directory already 38 | exists. 39 | 40 | Additionally, SecureRandom for some reason on linux 41 | is appears to be reading from /dev/random instead of 42 | /dev/urandom. As such, the many calls for temporary 43 | directories in the unit tests quickly depleates the 44 | entropy pool causing unit test runs to block until more 45 | entropy is collected (this is why moving the mouse speeds 46 | up unit tests). 47 | */ 48 | static { 49 | SecureRandom secRand = new SecureRandom(); 50 | rand = new Random(secRand.nextLong()); 51 | } 52 | 53 | /** 54 | * Private constructor as all methods are static. 55 | */ 56 | private FileTestUtils() { 57 | } 58 | 59 | /** 60 | * Convert a long to it's hex representation. Unlike 61 | * { 62 | * 63 | * @ Long#toHexString(long)} this always returns 16 digits. 64 | * @param l The long to convert. 65 | * @return l in hex. 66 | */ 67 | public static String toHexString(long l) { 68 | String initial; 69 | StringBuffer sb; 70 | 71 | initial = Long.toHexString(l); 72 | if (initial.length() == 16) { 73 | return initial; 74 | } 75 | sb = new StringBuffer(16); 76 | sb.append(initial); 77 | while (sb.length() < 16) { 78 | sb.insert(0, '0'); 79 | } 80 | return sb.toString(); 81 | } 82 | 83 | /** 84 | * Create a temporary directory. 85 | * 86 | * @param parent The parent directory for the temporary directory. If this is null, the system property "java.io.tmpdir" is used. 87 | * @param prefix The prefix for the directory's name. If this is null, the full class name of this class is used. 88 | * @param suffix The suffix for the directory's name. If this is null, ".tmp" is used. 89 | * @return The newly created temporary directory. 90 | * @throws IOException if directory creation fails 91 | * @throws SecurityException if {@link File#mkdir()} throws one. 92 | */ 93 | public static File createTmpDirectory(File parent, String prefix, String suffix) throws IOException { 94 | String name; 95 | File dir; 96 | 97 | if (prefix == null) { 98 | prefix = DEFAULT_PREFIX; 99 | } else if (!prefix.endsWith(".")) { 100 | prefix += '.'; 101 | } 102 | if (suffix == null) { 103 | suffix = DEFAULT_SUFFIX; 104 | } else if (!suffix.startsWith(".")) { 105 | suffix = "." + suffix; 106 | } 107 | if (parent == null) { 108 | parent = new File(System.getProperty("java.io.tmpdir")); 109 | } 110 | name = prefix + toHexString(rand.nextLong()) + suffix; 111 | dir = new File(parent, name); 112 | if (!dir.mkdir()) { 113 | throw new IOException("Unable to create temporary directory " + dir); 114 | } 115 | return dir.getCanonicalFile(); 116 | } 117 | 118 | /** 119 | * Create a temporary directory. This calls {@link #createTmpDirectory(File, String, String)} with null for parent and suffix. 120 | * 121 | * @param prefix The prefix for the directory's name. If this is null, the full class name of this class is used. 122 | * @return The newly created temporary directory. 123 | * @throws IOException if directory creation fails 124 | * @throws SecurityException if {@link File#mkdir()} throws one. 125 | */ 126 | public static File createTmpDirectory(String prefix) throws IOException { 127 | return createTmpDirectory(null, prefix, null); 128 | } 129 | 130 | /** 131 | * Create a temporary directory. This calls {@link #createTmpDirectory(File, String, String)} with null for all arguments. 132 | * 133 | * @return The newly created temporary directory. 134 | * @throws IOException if directory creation fails 135 | * @throws SecurityException if {@link File#mkdir()} throws one. 136 | */ 137 | public static File createTmpDirectory() throws IOException { 138 | return createTmpDirectory(null, null, null); 139 | } 140 | 141 | /** 142 | * Checks that child is a directory and really a child of parent. This verifies that the {@link File#getCanonicalFile() 143 | * canonical} child is actually a child of parent. This should fail if the child is a symbolic link to another directory and therefore should not be traversed in a recursive traversal of a 144 | * directory. 145 | * 146 | * @param parent The supposed parent of the child 147 | * @param child The child to check 148 | * @return true if child is a directory and a direct decendant of parent. 149 | * @throws IOException if {@link File#getCanonicalFile()} does 150 | * @throws NullPointerException if either parent or child are null. 151 | */ 152 | public static boolean isChildSubDirectory(File parent, File child) throws IOException { 153 | File childsParent; 154 | 155 | if (child == null) { 156 | throw new NullPointerException("child argument is null"); 157 | } 158 | if (!child.isDirectory()) { 159 | return false; 160 | } 161 | if (parent == null) { 162 | throw new NullPointerException("parent argument is null"); 163 | } 164 | parent = parent.getCanonicalFile(); 165 | child = child.getCanonicalFile(); 166 | childsParent = child.getParentFile(); 167 | if (childsParent == null) { 168 | return false; // sym link to /? 169 | } 170 | childsParent = childsParent.getCanonicalFile(); // just in case... 171 | if (!parent.equals(childsParent)) { 172 | return false; 173 | } 174 | return true; 175 | } 176 | 177 | /** 178 | * Delete a file. Unlinke {@link File#delete()}, this throws an exception if deletion fails. 179 | * 180 | * @param file The file to delete 181 | * @throws IOException if file is not null, exists but delete fails. 182 | */ 183 | public static void delete(File file) throws IOException { 184 | if (file == null || !file.exists()) { 185 | return; 186 | } 187 | if (!file.delete()) { 188 | throw new IOException("Unable to delete file " + file.getAbsolutePath()); 189 | } 190 | } 191 | 192 | /** 193 | * Recursively delete a file. If file is a directory, subdirectories and files are also deleted. Care is taken to not traverse symbolic links in this process. A null file or a file that does not 194 | * exist is considered to already been deleted. 195 | * 196 | * @param file The file or directory to be deleted 197 | * @throws IOException if the file, or a descendant, cannot be deleted. 198 | * @throws SecurityException if {@link File#delete()} does. 199 | */ 200 | public static void deleteRecursively(File file) throws IOException { 201 | File[] children; 202 | File child; 203 | 204 | if (file == null || !file.exists()) { 205 | return; // already deleted? 206 | } 207 | if (file.isDirectory()) { 208 | children = file.listFiles(); 209 | for (int i = 0; i < children.length; i++) { 210 | child = children[i]; 211 | if (isChildSubDirectory(file, child)) { 212 | deleteRecursively(child); 213 | } else { 214 | delete(child); 215 | } 216 | } 217 | } 218 | 219 | // finally 220 | delete(file); 221 | } 222 | } 223 | --------------------------------------------------------------------------------