|
20 | 20 | import java.io.File;
|
21 | 21 | import java.io.IOException;
|
22 | 22 | import java.io.InputStream;
|
| 23 | +import java.io.UnsupportedEncodingException; |
| 24 | +import java.net.URI; |
| 25 | +import java.net.URISyntaxException; |
| 26 | +import java.net.URLDecoder; |
23 | 27 | import java.text.DateFormat;
|
24 | 28 | import java.util.ArrayList;
|
25 | 29 | import java.util.Date;
|
| 30 | +import java.util.EnumMap; |
26 | 31 | import java.util.HashMap;
|
27 | 32 | import java.util.HashSet;
|
28 | 33 | import java.util.Iterator;
|
| 34 | +import java.util.LinkedHashMap; |
| 35 | +import java.util.LinkedList; |
29 | 36 | import java.util.List;
|
30 | 37 | import java.util.Map;
|
| 38 | +import java.util.Map.Entry; |
31 | 39 | import java.util.Set;
|
32 | 40 | import java.util.regex.Pattern;
|
33 | 41 |
|
34 | 42 | import javax.servlet.http.HttpServletRequest;
|
35 | 43 |
|
36 | 44 | import org.owasp.esapi.ESAPI;
|
37 | 45 | import org.owasp.esapi.Encoder;
|
| 46 | +import org.owasp.esapi.Logger; |
| 47 | +import org.owasp.esapi.SecurityConfiguration; |
38 | 48 | import org.owasp.esapi.ValidationErrorList;
|
39 | 49 | import org.owasp.esapi.ValidationRule;
|
40 | 50 | import org.owasp.esapi.Validator;
|
|
61 | 71 | * @see org.owasp.esapi.Validator
|
62 | 72 | */
|
63 | 73 | public class DefaultValidator implements org.owasp.esapi.Validator {
|
| 74 | + private static Logger logger = ESAPI.log(); |
64 | 75 | private static volatile Validator instance = null;
|
65 | 76 |
|
66 | 77 | public static Validator getInstance() {
|
@@ -1191,4 +1202,188 @@ private final boolean isEmpty(byte[] input) {
|
1191 | 1202 | private final boolean isEmpty(char[] input) {
|
1192 | 1203 | return (input==null || input.length == 0);
|
1193 | 1204 | }
|
| 1205 | + |
| 1206 | + /** |
| 1207 | + * {@inheritDoc} |
| 1208 | + */ |
| 1209 | + public boolean isValidURI(String context, String input, boolean allowNull) { |
| 1210 | + boolean isValid = false; |
| 1211 | + URI compliantURI = this.getRfcCompliantURI(input); |
| 1212 | + |
| 1213 | + try{ |
| 1214 | + if(null != compliantURI){ |
| 1215 | + String canonicalizedURI = getCanonicalizedURI(compliantURI); |
| 1216 | + //if getCanonicalizedURI doesn't throw an IntrusionException, then the URI contains no mixed or |
| 1217 | + //double-encoding attacks. |
| 1218 | + logger.info(Logger.SECURITY_SUCCESS, "We did not detect any mixed or multiple encoding in the uri:[" + input + "]"); |
| 1219 | + Validator v = ESAPI.validator(); |
| 1220 | + //This part will use the regex from validation.properties. This regex should be super-simple, and |
| 1221 | + //used mainly to restrict certain parts of a URL. |
| 1222 | + Pattern p = ESAPI.securityConfiguration().getValidationPattern( "URL" ); |
| 1223 | + //We're doing this instead of using the normal validator API, because it will canonicalize the input again |
| 1224 | + //and if the URI has any queries that also happen to match HTML entities, like ¶ |
| 1225 | + //it will cease conforming to the regex we now specify for a URL. |
| 1226 | + isValid = p.matcher(canonicalizedURI).matches(); |
| 1227 | + } |
| 1228 | + |
| 1229 | + }catch (IntrusionException e){ |
| 1230 | + logger.error(Logger.SECURITY_FAILURE, e.getMessage()); |
| 1231 | + isValid = false; |
| 1232 | + } |
| 1233 | + |
| 1234 | + |
| 1235 | + return isValid; |
| 1236 | + } |
| 1237 | + |
| 1238 | + /** |
| 1239 | + * {@inheritDoc} |
| 1240 | + */ |
| 1241 | + public URI getRfcCompliantURI(String input){ |
| 1242 | + URI rval = null; |
| 1243 | + try { |
| 1244 | + rval = new URI(input); |
| 1245 | + } catch (URISyntaxException e) { |
| 1246 | + logger.error(Logger.EVENT_FAILURE, e.getMessage()); |
| 1247 | + } |
| 1248 | + return rval; |
| 1249 | + } |
| 1250 | + |
| 1251 | + /** |
| 1252 | + * This does alot. This will extract each piece of a URI according to parse zone, and it will construct |
| 1253 | + * a canonicalized String representing a version of the URI that is safe to run regex against to it. |
| 1254 | + * |
| 1255 | + * @param dirtyUri |
| 1256 | + * @return |
| 1257 | + * @throws IntrusionException |
| 1258 | + */ |
| 1259 | + public String getCanonicalizedURI(URI dirtyUri) throws IntrusionException{ |
| 1260 | + |
| 1261 | +// From RFC-3986 section 3 |
| 1262 | +// URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] |
| 1263 | +// |
| 1264 | +// hier-part = "//" authority path-abempty |
| 1265 | +// / path-absolute |
| 1266 | +// / path-rootless |
| 1267 | +// / path-empty |
| 1268 | + |
| 1269 | +// The following are two example URIs and their component parts: |
| 1270 | +// |
| 1271 | +// foo://example.com:8042/over/there?name=ferret#nose |
| 1272 | +// \_/ \______________/\_________/ \_________/ \__/ |
| 1273 | +// | | | | | |
| 1274 | +// scheme authority path query fragment |
| 1275 | +// | _____________________|__ |
| 1276 | +// / \ / \ |
| 1277 | +// urn:example:animal:ferret:nose |
| 1278 | + Map<UriSegment, String> parseMap = new EnumMap<UriSegment, String>(UriSegment.class); |
| 1279 | + parseMap.put(UriSegment.SCHEME, dirtyUri.getScheme()); |
| 1280 | + //authority = [ userinfo "@" ] host [ ":" port ] |
| 1281 | + parseMap.put(UriSegment.AUTHORITY, dirtyUri.getRawAuthority()); |
| 1282 | + parseMap.put(UriSegment.SCHEMSPECIFICPART, dirtyUri.getRawSchemeSpecificPart()); |
| 1283 | + parseMap.put(UriSegment.HOST, dirtyUri.getHost()); |
| 1284 | + //if port is undefined, it will return -1 |
| 1285 | + Integer port = new Integer(dirtyUri.getPort()); |
| 1286 | + parseMap.put(UriSegment.PORT, port == -1 ? "": port.toString()); |
| 1287 | + parseMap.put(UriSegment.PATH, dirtyUri.getRawPath()); |
| 1288 | + parseMap.put(UriSegment.QUERY, dirtyUri.getRawQuery()); |
| 1289 | + parseMap.put(UriSegment.FRAGMENT, dirtyUri.getRawFragment()); |
| 1290 | + |
| 1291 | + //Now we canonicalize each part and build our string. |
| 1292 | + StringBuilder sb = new StringBuilder(); |
| 1293 | + |
| 1294 | + //Replace all the items in the map with canonicalized versions. |
| 1295 | + |
| 1296 | + Set<UriSegment> set = parseMap.keySet(); |
| 1297 | + |
| 1298 | + SecurityConfiguration sg = ESAPI.securityConfiguration(); |
| 1299 | +// boolean restrictMixed = sg.getBooleanProp("AllowMixedEncoding"); |
| 1300 | +// boolean restrictMultiple = sg.getBooleanProp("AllowMultipleEncoding"); |
| 1301 | + boolean allowMixed = sg.getAllowMixedEncoding(); |
| 1302 | + boolean allowMultiple = sg.getAllowMultipleEncoding(); |
| 1303 | + for(UriSegment seg: set){ |
| 1304 | + String value = encoder.canonicalize(parseMap.get(seg), allowMultiple, allowMixed); |
| 1305 | + value = value == null ? "" : value; |
| 1306 | + //In the case of a uri query, we need to break up and canonicalize the internal parts of the query. |
| 1307 | + if(seg == UriSegment.QUERY && null != parseMap.get(seg)){ |
| 1308 | + StringBuilder qBuilder = new StringBuilder(); |
| 1309 | + try { |
| 1310 | + Map<String, List<String>> canonicalizedMap = this.splitQuery(dirtyUri); |
| 1311 | + Set<Entry<String, List<String>>> query = canonicalizedMap.entrySet(); |
| 1312 | + Iterator<Entry<String, List<String>>> i = query.iterator(); |
| 1313 | + while(i.hasNext()){ |
| 1314 | + Entry<String, List<String>> e = i.next(); |
| 1315 | + String key = (String) e.getKey(); |
| 1316 | + String qVal = ""; |
| 1317 | + List<String> list = (List<String>) e.getValue(); |
| 1318 | + if(!list.isEmpty()){ |
| 1319 | + qVal = list.get(0); |
| 1320 | + } |
| 1321 | + qBuilder.append(key) |
| 1322 | + .append("=") |
| 1323 | + .append(qVal); |
| 1324 | + |
| 1325 | + if(i.hasNext()){ |
| 1326 | + qBuilder.append("&"); |
| 1327 | + } |
| 1328 | + } |
| 1329 | + value = qBuilder.toString(); |
| 1330 | + } catch (UnsupportedEncodingException e) { |
| 1331 | + logger.debug(Logger.EVENT_FAILURE, "decoding error when parsing [" + dirtyUri.toString() + "]"); |
| 1332 | + } |
| 1333 | + } |
| 1334 | + parseMap.put(seg, value ); |
| 1335 | + } |
| 1336 | + |
| 1337 | + return buildUrl(parseMap); |
| 1338 | + } |
| 1339 | + |
| 1340 | +/** |
| 1341 | + * The meat of this method was taken from StackOverflow: http://stackoverflow.com/a/13592567/557153 |
| 1342 | + * It has been modified to return a canonicalized key and value pairing. |
| 1343 | + * |
| 1344 | + * @param java URI |
| 1345 | + * @return a map of canonicalized query parameters. |
| 1346 | + * @throws UnsupportedEncodingException |
| 1347 | + */ |
| 1348 | + public Map<String, List<String>> splitQuery(URI uri) throws UnsupportedEncodingException { |
| 1349 | + final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>(); |
| 1350 | + final String[] pairs = uri.getQuery().split("&"); |
| 1351 | + for (String pair : pairs) { |
| 1352 | + final int idx = pair.indexOf("="); |
| 1353 | + final String key = idx > 0 ? encoder.canonicalize(pair.substring(0, idx)) : pair; |
| 1354 | + if (!query_pairs.containsKey(key)) { |
| 1355 | + query_pairs.put(key, new LinkedList<String>()); |
| 1356 | + } |
| 1357 | + final String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : null; |
| 1358 | + query_pairs.get(key).add(encoder.canonicalize(value)); |
| 1359 | + } |
| 1360 | + return query_pairs; |
| 1361 | + } |
| 1362 | + |
| 1363 | + public enum UriSegment { |
| 1364 | + AUTHORITY, SCHEME, SCHEMSPECIFICPART, USERINFO, HOST, PORT, PATH, QUERY, FRAGMENT |
| 1365 | + } |
| 1366 | + |
| 1367 | + /** |
| 1368 | + * All the parts should be canonicalized by this point. This is straightforward assembly. |
| 1369 | + * |
| 1370 | + * @param set |
| 1371 | + * @return |
| 1372 | + */ |
| 1373 | + protected String buildUrl(Map<UriSegment, String> parseMap){ |
| 1374 | + StringBuilder sb = new StringBuilder(); |
| 1375 | + sb.append(parseMap.get(UriSegment.SCHEME)) |
| 1376 | + .append("://") |
| 1377 | + //can't use SCHEMESPECIFICPART for this, because we need to canonicalize all the parts of the query. |
| 1378 | + //USERINFO is also deprecated. So we technically have more than we need. |
| 1379 | + .append(parseMap.get(UriSegment.AUTHORITY) == null || parseMap.get(UriSegment.AUTHORITY).equals("") ? "" : parseMap.get(UriSegment.AUTHORITY)) |
| 1380 | + .append(parseMap.get(UriSegment.PATH) == null || parseMap.get(UriSegment.PATH).equals("") ? "" : parseMap.get(UriSegment.PATH)) |
| 1381 | + .append(parseMap.get(UriSegment.QUERY) == null || parseMap.get(UriSegment.QUERY).equals("") |
| 1382 | + ? "" : "?" + parseMap.get(UriSegment.QUERY)) |
| 1383 | + .append((parseMap.get(UriSegment.FRAGMENT) == null) || parseMap.get(UriSegment.FRAGMENT).equals("") |
| 1384 | + ? "": "#" + parseMap.get(UriSegment.FRAGMENT)) |
| 1385 | + ; |
| 1386 | + return sb.toString(); |
| 1387 | + } |
| 1388 | + |
1194 | 1389 | }
|
0 commit comments