All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.yaas.servicesdk.security.SecurityUtils Maven / Gradle / Ivy

/*
 * © 2016 SAP SE or an SAP affiliate company.
 * All rights reserved.
 * Please see http://www.sap.com/corporate-en/legal/copyright/index.epx for additional trademark information and
 * notices.
 */

package com.sap.cloud.yaas.servicesdk.security;

import java.net.URI;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Security utility class for sanitising of the input of web applications to avoid path manipulation violations.
 */
public final class SecurityUtils
{
	private static final Logger LOG = LoggerFactory.getLogger(SecurityUtils.class);

	private static final Pattern PERMITTED_PATH_SEGMENT_PATTERN = Pattern.compile("[-_.a-zA-Z0-9]*");
	private static final Pattern FORBIDDEN_PATH_SEGMENT_PATTERN = Pattern.compile("[.]?[.]?");

	/**
	 * A black-list of the chars in the file name, everything which is not an alphanumeric, any white space,
	 * minus (-) or underline (_).
	 */
	private static final String NOT_ALLOWED_CHARS = "[^a-zA-Z\\d\\s_-]";
	/**
	 * A white-list of the allowed files extensions.
	 */
	private static final String[] FILE_EXTENSIONS_WHITE_LIST = {"yaml", "json", "txt", "xml", "raml"};

	private static final Pattern NOT_ALLOWED_CHARS_PATTERN = Pattern.compile(NOT_ALLOWED_CHARS);
	private static final int FILE_WITH_EXTENSION = 2;
	private static final String DOT = ".";

	private SecurityUtils()
	{
		// avoid construction
	}

	/**
	 * Method to sanitize given path accordingly to allowed root application folder.
	 *
	 * @param servletRootPath a top level accessible folder path
	 * @param requestedPath the requested relative path
	 * @return a full path relative to allowed top level folder
	 * @throws PathTraversalException throw in case the given requestedPath is
	 *            
    *
  • absolute path
  • *
  • can not be represented in canonical way
  • *
  • is traverses up the the folder structure out of the servletRootPath bounds
  • *
*/ public static Path sanitizePath(final Path servletRootPath, final Path requestedPath)// throws PathTraversalException { if (requestedPath.isAbsolute()) { LOG.error("Not allowed to access directly absolute path " + requestedPath); throw new PathTraversalException("Not allowed to access directly absolute paths."); } final Path normalizedRequestedPath = servletRootPath.resolve(requestedPath).normalize(); final Path normalizedServletRootPath = servletRootPath.normalize(); if (!normalizedRequestedPath.startsWith(normalizedServletRootPath)) { LOG.error("Requested resource " + requestedPath + " is not relatively located inside the allowed web application " + "root folder " + servletRootPath); throw new PathTraversalException("Requested resource is not relatively located inside the allowed web application " + "root folder."); } return normalizedRequestedPath; } /** * Method sanitizes the file name itself if it doesn't contain any disallowed characters {@link #NOT_ALLOWED_CHARS}. * * @param requestedFile a given file path * @return a validated file name * @throws PathTraversalException in case the file name contains ta least one not allowed char * {@link #NOT_ALLOWED_CHARS} */ public static String sanitizeFileName(final Path requestedFile)// throws PathTraversalException { return sanitizeFileName(requestedFile.toString()); } /** * Method sanitizes the file name itself if it doesn't contain any disallowed characters {@link #NOT_ALLOWED_CHARS}. * * @param requestedFileName a given file name * @return a validated file name * @throws PathTraversalException in case the file name contains ta least one not allowed char * {@link #NOT_ALLOWED_CHARS} */ public static String sanitizeFileName(final String requestedFileName)// throws PathTraversalException { final StringTokenizer tokenizer = new StringTokenizer(requestedFileName, DOT); if (tokenizer.countTokens() == FILE_WITH_EXTENSION) { final String fileName = tokenizer.nextToken(); final String fileExtension = tokenizer.nextToken(); final Matcher matcher = NOT_ALLOWED_CHARS_PATTERN.matcher(fileName); if (matcher.find()) { LOG.error("Given path " + fileName + " contains not allowed characters"); throw new PathTraversalException("Given path contains not allowed characters."); } return String.format("%s.%s", fileName, validateFileExtension(fileExtension)); } else { LOG.error("Given filename " + requestedFileName + " contains somehow misleading or none file extension."); throw new PathTraversalException("Given filename contains somehow misleading or none file extension."); } } /** * Asserts that a given String represents a single path segment that can securely be used to access a file-system or * classpath resource. *

* This assertion is performed in a platform independent but very conservative manner. In particular, the following * conditions must be met: *
* * The pathSegment may only contain ASCII letters and digits, as well as the characters dash, underscore, and * period characters. *
* * Consequently the pathSegment must not contain common separators like slash or backslash. *
* * Also, the pathSegment must not contain control characters or the percent character, which is used in * URL-encoding. *
* * The pathSegment must not equal a single period or a sequence of two periods. (These represent the current * directory and the parent directory respectively on many file-systems.) *
* * The pathSegment must not be empty. * * @param pathSegment the path segment to check * @return a validated path segment * @throws PathTraversalException the pathSegment is not considered secure. */ public static String sanitizePathSegment(final String pathSegment) throws PathTraversalException { if (!PERMITTED_PATH_SEGMENT_PATTERN.matcher(pathSegment).matches()) { throw new PathTraversalException("Path component " + pathSegment + " does not match the permitted pattern " + PERMITTED_PATH_SEGMENT_PATTERN + ", which might constitute the attempt of a path traversal attack."); } if (FORBIDDEN_PATH_SEGMENT_PATTERN.matcher(pathSegment).matches()) { throw new PathTraversalException("Path component " + pathSegment + " matches the forbidden pattern " + FORBIDDEN_PATH_SEGMENT_PATTERN + ", which might constitute the attempt of a path traversal attack."); } return pathSegment; } /** * Validates the file's extension against the white-list. */ private static String validateFileExtension(final String fileExtension) { for (final String extension : FILE_EXTENSIONS_WHITE_LIST) { if (extension.equalsIgnoreCase(fileExtension)) { return fileExtension; } } LOG.error("Given file.extension " + fileExtension + " is not found among allowed file types "// + Arrays.toString(FILE_EXTENSIONS_WHITE_LIST)); throw new PathTraversalException("Given file type is not supported."); } /** * Makes sure that the URI starts with "http" or "https", to avoid access to local files for example. * Relative paths are also fine. If URI is null, null is returned. * * @param uri the URI to sanitize * @return the URI object in case it is valid * @throws IllegalArgumentException in case the URI is not valid */ public static URI sanitizeRemoteUrl(final URI uri) { if (!isValidRemoteUri(uri)) { throw new IllegalArgumentException("Only http(s) and relative paths are supported."); } return uri; } /** * Makes sure that the URI starts with "http" or "https", to avoid access to local files for example. * Relative paths are also fine. If URI is null, null is returned. * * @param uri the URI to sanitize * @return true if the URI is valid */ public static boolean isValidRemoteUri(final URI uri) { return uri == null || uri.getScheme() == null || "http".equalsIgnoreCase(uri.getScheme()) || "https".equalsIgnoreCase(uri.getScheme()); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy