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 "" + 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 extends CharSequence, ? extends T> map) {
528 | for (Map.Entry extends CharSequence, ? extends T> 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 extends CharSequence, ? extends T> 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 extends CharSequence, ? extends T> 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 |
--------------------------------------------------------------------------------