Skip to content
35 changes: 34 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>findbugs-maven-plugin</artifactId>
<version>2.5.5</version>
<version>3.0.4</version>
<configuration>
<findbugsXmlOutput>true</findbugsXmlOutput>
<findbugsXmlWithMessages>true</findbugsXmlWithMessages>
Expand Down Expand Up @@ -412,6 +412,39 @@
</reporting>

<profiles>
<profile>
<id>doclint-java8-disable</id>
<activation>
<jdk>[1.8,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.4</version>
<configuration>
<reportPlugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</plugin>
</reportPlugins>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<!-- Activate to sign jars and build distributable download. -->
<id>dist</id>
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/owasp/esapi/Encoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.owasp.esapi;

import java.io.IOException;
import java.net.URI;

import org.owasp.esapi.codecs.Codec;
import org.owasp.esapi.errors.EncodingException;
Expand Down Expand Up @@ -513,4 +514,15 @@ public interface Encoder {
*/
byte[] decodeFromBase64(String input) throws IOException;

/**
*
* Get a version of the input URI that will be safe to run regex and other validations against.
* It is not recommended to persist this value as it will transform user input. This method
* will not test to see if the URI is RFC-3986 compliant.
*
* @param input
* @return
*/
public String getCanonicalizedURI(URI dirtyUri);

}
11 changes: 0 additions & 11 deletions src/main/java/org/owasp/esapi/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -708,17 +708,6 @@ public interface Validator {
*/
boolean isValidURI(String context, String input, boolean allowNull);

/**
*
* Get a version of the input URI that will be safe to run regex and other validations against.
* It is not recommended to persist this value as it will transform user input. This method
* will not test to see if the URI is RFC-3986 compliant.
*
* @param input
* @return
*/
public String getCanonicalizedURI(URI dirtyUri);

/**
* Will return a {@code URI} object that will represent a fully parsed and legal URI
* as specified in RFC-3986.
Expand Down
154 changes: 154 additions & 0 deletions src/main/java/org/owasp/esapi/reference/DefaultEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,23 @@

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Encoder;
import org.owasp.esapi.Logger;
import org.owasp.esapi.SecurityConfiguration;
import org.owasp.esapi.codecs.Base64;
import org.owasp.esapi.codecs.CSSCodec;
import org.owasp.esapi.codecs.Codec;
Expand Down Expand Up @@ -445,4 +453,150 @@ public byte[] decodeFromBase64(String input) throws IOException {
}
return Base64.decode( input );
}

/**
* {@inheritDoc}
*
* This will extract each piece of a URI according to parse zone as specified in <a href="https://www.ietf.org/rfc/rfc3986.txt">RFC-3986</a> section 3,
* and it will construct a canonicalized String representing a version of the URI that is safe to
* run regex against.
*
* @param dirtyUri
* @return Canonicalized URI string.
* @throws IntrusionException
*/
public String getCanonicalizedURI(URI dirtyUri) throws IntrusionException{

// From RFC-3986 section 3
// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
//
// hier-part = "//" authority path-abempty
// / path-absolute
// / path-rootless
// / path-empty

// The following are two example URIs and their component parts:
//
// foo://example.com:8042/over/there?name=ferret#nose
// \_/ \______________/\_________/ \_________/ \__/
// | | | | |
// scheme authority path query fragment
// | _____________________|__
// / \ / \
// urn:example:animal:ferret:nose
Map<UriSegment, String> parseMap = new EnumMap<UriSegment, String>(UriSegment.class);
parseMap.put(UriSegment.SCHEME, dirtyUri.getScheme());
//authority = [ userinfo "@" ] host [ ":" port ]
parseMap.put(UriSegment.AUTHORITY, dirtyUri.getRawAuthority());
parseMap.put(UriSegment.SCHEMSPECIFICPART, dirtyUri.getRawSchemeSpecificPart());
parseMap.put(UriSegment.HOST, dirtyUri.getHost());
//if port is undefined, it will return -1
Integer port = new Integer(dirtyUri.getPort());
parseMap.put(UriSegment.PORT, port == -1 ? "": port.toString());
parseMap.put(UriSegment.PATH, dirtyUri.getRawPath());
parseMap.put(UriSegment.QUERY, dirtyUri.getRawQuery());
parseMap.put(UriSegment.FRAGMENT, dirtyUri.getRawFragment());

//Now we canonicalize each part and build our string.
StringBuilder sb = new StringBuilder();

//Replace all the items in the map with canonicalized versions.

Set<UriSegment> set = parseMap.keySet();

SecurityConfiguration sg = ESAPI.securityConfiguration();
boolean allowMixed = sg.getBooleanProp("Encoder.AllowMixedEncoding");
boolean allowMultiple = sg.getBooleanProp("Encoder.AllowMultipleEncoding");
for(UriSegment seg: set){
String value = canonicalize(parseMap.get(seg), allowMultiple, allowMixed);
value = value == null ? "" : value;
//In the case of a uri query, we need to break up and canonicalize the internal parts of the query.
if(seg == UriSegment.QUERY && null != parseMap.get(seg)){
StringBuilder qBuilder = new StringBuilder();
try {
Map<String, List<String>> canonicalizedMap = this.splitQuery(dirtyUri);
Set<Entry<String, List<String>>> query = canonicalizedMap.entrySet();
Iterator<Entry<String, List<String>>> i = query.iterator();
while(i.hasNext()){
Entry<String, List<String>> e = i.next();
String key = (String) e.getKey();
String qVal = "";
List<String> list = (List<String>) e.getValue();
if(!list.isEmpty()){
qVal = list.get(0);
}
qBuilder.append(key)
.append("=")
.append(qVal);

if(i.hasNext()){
qBuilder.append("&");
}
}
value = qBuilder.toString();
} catch (UnsupportedEncodingException e) {
logger.debug(Logger.EVENT_FAILURE, "decoding error when parsing [" + dirtyUri.toString() + "]");
}
}
//Check if the port is -1, if it is, omit it from the output.
if(seg == UriSegment.PORT){
if("-1" == parseMap.get(seg)){
value = "";
}
}
parseMap.put(seg, value );
}

return buildUrl(parseMap);
}

/**
* All the parts should be canonicalized by this point. This is straightforward assembly.
*
* @param set
* @return
*/
protected String buildUrl(Map<UriSegment, String> parseMap){
StringBuilder sb = new StringBuilder();
sb.append(parseMap.get(UriSegment.SCHEME))
.append("://")
//can't use SCHEMESPECIFICPART for this, because we need to canonicalize all the parts of the query.
//USERINFO is also deprecated. So we technically have more than we need.
.append(parseMap.get(UriSegment.AUTHORITY) == null || parseMap.get(UriSegment.AUTHORITY).equals("") ? "" : parseMap.get(UriSegment.AUTHORITY))
.append(parseMap.get(UriSegment.PATH) == null || parseMap.get(UriSegment.PATH).equals("") ? "" : parseMap.get(UriSegment.PATH))
.append(parseMap.get(UriSegment.QUERY) == null || parseMap.get(UriSegment.QUERY).equals("")
? "" : "?" + parseMap.get(UriSegment.QUERY))
.append((parseMap.get(UriSegment.FRAGMENT) == null) || parseMap.get(UriSegment.FRAGMENT).equals("")
? "": "#" + parseMap.get(UriSegment.FRAGMENT))
;
return sb.toString();
}

public enum UriSegment {
AUTHORITY, SCHEME, SCHEMSPECIFICPART, USERINFO, HOST, PORT, PATH, QUERY, FRAGMENT
}


/**
* The meat of this method was taken from StackOverflow: http://stackoverflow.com/a/13592567/557153
* It has been modified to return a canonicalized key and value pairing.
*
* @param java URI
* @return a map of canonicalized query parameters.
* @throws UnsupportedEncodingException
*/
public Map<String, List<String>> splitQuery(URI uri) throws UnsupportedEncodingException {
final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
final String[] pairs = uri.getQuery().split("&");
for (String pair : pairs) {
final int idx = pair.indexOf("=");
final String key = idx > 0 ? canonicalize(pair.substring(0, idx)) : pair;
if (!query_pairs.containsKey(key)) {
query_pairs.put(key, new LinkedList<String>());
}
final String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null;
query_pairs.get(key).add(canonicalize(value));
}
return query_pairs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.HTTPUtilities;
import org.owasp.esapi.Logger;
import org.owasp.esapi.SecurityConfiguration;
import org.owasp.esapi.StringUtilities;
import org.owasp.esapi.User;
import org.owasp.esapi.ValidationErrorList;
Expand Down Expand Up @@ -929,6 +930,9 @@ public String setRememberToken( HttpServletRequest request, HttpServletResponse
String clearToken = user.getAccountName() + "|" + password;
long expiry = ESAPI.encryptor().getRelativeTimeStamp(maxAge * 1000);
String cryptToken = ESAPI.encryptor().seal(clearToken, expiry);
SecurityConfiguration sg = ESAPI.securityConfiguration();
boolean forceSecureCookies = sg.getBooleanProp("HttpUtilities.ForceSecureCookies");
boolean forceHttpOnly = sg.getBooleanProp("HttpUtilities.ForceHttpOnlyCookies");

// Do NOT URLEncode cryptToken before creating cookie. See Google Issue # 144,
// which was marked as "WontFix".
Expand All @@ -937,6 +941,8 @@ public String setRememberToken( HttpServletRequest request, HttpServletResponse
cookie.setMaxAge( maxAge );
cookie.setDomain( domain );
cookie.setPath( path );
cookie.setHttpOnly(forceHttpOnly);
cookie.setSecure(forceSecureCookies);
response.addCookie( cookie );
logger.info(Logger.SECURITY_SUCCESS, "Enabled remember me token for " + user.getAccountName() );
return cryptToken;
Expand All @@ -957,14 +963,18 @@ public String setRememberToken(HttpServletRequest request, HttpServletResponse r
String clearToken = user.getAccountName();
long expiry = ESAPI.encryptor().getRelativeTimeStamp(maxAge * 1000);
String cryptToken = ESAPI.encryptor().seal(clearToken, expiry);

SecurityConfiguration sg = ESAPI.securityConfiguration();
boolean forceSecureCookies = sg.getBooleanProp("HttpUtilities.ForceSecureCookies");
boolean forceHttpOnly = sg.getBooleanProp("HttpUtilities.ForceHttpOnlyCookies");
// Do NOT URLEncode cryptToken before creating cookie. See Google Issue # 144,
// which was marked as "WontFix".

Cookie cookie = new Cookie( REMEMBER_TOKEN_COOKIE_NAME, cryptToken );
cookie.setMaxAge( maxAge );
cookie.setDomain( domain );
cookie.setPath( path );
cookie.setHttpOnly(forceHttpOnly);
cookie.setSecure(forceSecureCookies);
response.addCookie( cookie );
logger.info(Logger.SECURITY_SUCCESS, "Enabled remember me token for " + user.getAccountName() );
} catch( IntegrityException e){
Expand Down
Loading