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());
}
}