-
Notifications
You must be signed in to change notification settings - Fork 365
/
Copy pathHTMLValidationRule.java
262 lines (220 loc) · 11.6 KB
/
HTMLValidationRule.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
/**
* OWASP Enterprise Security API (ESAPI)
*
* This file is part of the Open Web Application Security Project (OWASP)
* Enterprise Security API (ESAPI) project. For details, please see
* <a href="http://www.owasp.org/index.php/ESAPI">http://www.owasp.org/index.php/ESAPI</a>.
*
* Copyright (c) 2007 - The OWASP Foundation
*
* The ESAPI is published by OWASP under the BSD license. You should read and accept the
* LICENSE before you use, modify, and/or redistribute this software.
*
* @author Jeff Williams <a href="http://www.aspectsecurity.com">Aspect Security</a>
* @created 2007
*/
package org.owasp.esapi.reference.validation;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Encoder;
import org.owasp.esapi.Logger;
import org.owasp.esapi.SecurityConfiguration;
import org.owasp.esapi.StringUtilities;
import org.owasp.esapi.errors.ConfigurationException;
import org.owasp.esapi.errors.ValidationException;
import org.owasp.esapi.PropNames.DefaultSearchPath;
import static org.owasp.esapi.PropNames.VALIDATOR_HTML_VALIDATION_ACTION;
import static org.owasp.esapi.PropNames.VALIDATOR_HTML_VALIDATION_CONFIGURATION_FILE;
import org.owasp.validator.html.AntiSamy;
import org.owasp.validator.html.CleanResults;
import org.owasp.validator.html.Policy;
import org.owasp.validator.html.PolicyException;
import org.owasp.validator.html.ScanException;
/**
* A validator performs syntax and possibly semantic validation of a single
* piece of data from an untrusted source.
*
* @author Jeff Williams (jeff.williams .at. aspectsecurity.com) <a
* href="http://www.aspectsecurity.com">Aspect Security</a>
* @since June 1, 2007
* @see org.owasp.esapi.Validator
*/
public class HTMLValidationRule extends StringValidationRule {
/** OWASP AntiSamy markup verification policy */
private static Policy antiSamyPolicy = null;
private static final Logger LOGGER = ESAPI.getLogger( "HTMLValidationRule" );
private static final String ANTISAMYPOLICY_FILENAME = "antisamy-esapi.xml";
/*package */ static InputStream getResourceStreamFromClassLoader(String contextDescription, ClassLoader classLoader, String fileName, List<String> searchPaths) {
InputStream result = null;
for (String searchPath: searchPaths) {
result = classLoader.getResourceAsStream(searchPath + fileName);
if (result != null) {
LOGGER.info(Logger.EVENT_SUCCESS, "SUCCESSFULLY LOADED " + fileName + " via the CLASSPATH from '" +
searchPath + "' using " + contextDescription + "!");
break;
}
}
return result;
}
/*package */ static InputStream getResourceStreamFromClasspath(String fileName) {
LOGGER.info(Logger.EVENT_FAILURE, "Loading " + fileName + " from classpaths");
InputStream resourceStream = null;
List<String> orderedSearchPaths = Arrays.asList(DefaultSearchPath.ROOT.value(),
DefaultSearchPath.RESOURCE_DIRECTORY.value(),
DefaultSearchPath.DOT_ESAPI.value(),
DefaultSearchPath.ESAPI.value(),
DefaultSearchPath.RESOURCES.value(),
DefaultSearchPath.SRC_MAIN_RESOURCES.value());
resourceStream = getResourceStreamFromClassLoader("current thread context class loader", Thread.currentThread().getContextClassLoader(), fileName, orderedSearchPaths);
//I'm lazy. Using ternary for shorthand "if null then do next thing" Harder to read, sorry
resourceStream = resourceStream != null ? resourceStream : getResourceStreamFromClassLoader("system class loader", ClassLoader.getSystemClassLoader(), fileName, orderedSearchPaths);
resourceStream = resourceStream != null ? resourceStream : getResourceStreamFromClassLoader("class loader for DefaultSecurityConfiguration class", ESAPI.securityConfiguration().getClass().getClassLoader(), fileName, orderedSearchPaths);
return resourceStream;
}
/*package */ static Policy loadAntisamyPolicy(String antisamyPolicyFilename) throws IOException, PolicyException {
InputStream resourceStream = null;
SecurityConfiguration secCfg = ESAPI.securityConfiguration();
//Rather than catching the IOException from the resource stream, let's ask if the file exists to give this a best-case resolution.
//This helps with the IOException handling too. If the file is there and we get an IOException from the SecurityConfiguration, then the file is there and something else is wrong (FAIL -- don't try the other path)
File file = secCfg.getResourceFile(antisamyPolicyFilename);
resourceStream = file == null ? getResourceStreamFromClasspath(antisamyPolicyFilename) : secCfg.getResourceStream(antisamyPolicyFilename);
return resourceStream == null ? null : Policy.getInstance(resourceStream);
}
/*package */ static String resolveAntisamyFilename() {
String antisamyPolicyFilename = ANTISAMYPOLICY_FILENAME;
try {
antisamyPolicyFilename = ESAPI.securityConfiguration().getStringProp( VALIDATOR_HTML_VALIDATION_CONFIGURATION_FILE );
} catch (ConfigurationException cex) {
LOGGER.info(Logger.EVENT_FAILURE, "ESAPI property " +
VALIDATOR_HTML_VALIDATION_CONFIGURATION_FILE +
" not set, using default value: " + ANTISAMYPOLICY_FILENAME);
}
return antisamyPolicyFilename;
}
/*package */ static void configureInstance() {
String antisamyPolicyFilename = resolveAntisamyFilename();
try {
antiSamyPolicy = loadAntisamyPolicy(antisamyPolicyFilename);
} catch (IOException ioe) {
//Thrown if file is found by SecurityConfiguration, but a stream cannot be generated.
throw new ConfigurationException("Failed to load file from SecurityConfiguration context: " + antisamyPolicyFilename, ioe);
} catch (PolicyException e) {
//Thrown if the resource stream was created, but the contents of the file are not compatible with antisamy expectations.
throw new ConfigurationException("Couldn't parse " + antisamyPolicyFilename, e);
}
if (antiSamyPolicy == null) {
throw new ConfigurationException("Couldn't find " + antisamyPolicyFilename);
}
}
static {
configureInstance();
}
public HTMLValidationRule( String typeName ) {
super( typeName );
}
public HTMLValidationRule( String typeName, Encoder encoder ) {
super( typeName, encoder );
}
public HTMLValidationRule( String typeName, Encoder encoder, String whitelistPattern ) {
super( typeName, encoder, whitelistPattern );
}
/**
* {@inheritDoc}
*/
@Override
public String getValid( String context, String input ) throws ValidationException {
return invokeAntiSamy( context, input );
}
/**
* {@inheritDoc}
*/
@Override
public String sanitize( String context, String input ) {
String safe = "";
try {
safe = invokeAntiSamy( context, input );
} catch( ValidationException e ) {
// just return safe
}
return safe;
}
/**
* Check whether we want the legacy behavior ("clean") or the presumably intended
* behavior of "throw" for how to treat unsafe HTML input when AntiSamy is invoked.
* This admittedly is an UGLY hack to ensure that issue 509 and its corresponding
* fix in PR #510 does not break existing developer's existing code. Full
* details are described in GitHub issue 521.
*
* Checks new ESAPI property "Validator.HtmlValidationAction". A value of "clean"
* means to revert to legacy behavior. A value of "throw" means to use the new
* behavior as implemented in GitHub issue 509.
*
* @return false - If "Validator.HtmlValidationAction" is set to "throw". Otherwise {@code true}.
* @since 2.2.1.0
*/
private boolean legacyHtmlValidation() {
boolean legacy = true; // Make legacy support the default behavior for backward compatibility.
String propValue = "clean"; // For legacy support.
try {
// DISCUSS:
// Hindsight: maybe we should have getBooleanProp(), getStringProp(),
// getIntProp() methods that take a default arg as well?
// At least for ESAPI 3.x.
propValue = ESAPI.securityConfiguration().getStringProp( VALIDATOR_HTML_VALIDATION_ACTION );
switch ( propValue.toLowerCase() ) {
case "throw":
legacy = false; // New, presumably correct behavior, as addressed by GitHub issue 509
break;
case "clean":
legacy = true; // Give the caller that legacy behavior of sanitizing.
break;
default:
LOGGER.warning(Logger.EVENT_FAILURE, "ESAPI property " +
VALIDATOR_HTML_VALIDATION_ACTION +
" was set to \"" + propValue + "\". Must be set to either \"clean\"" +
" (the default for legacy support) or \"throw\"; assuming \"clean\" for legacy behavior.");
legacy = true;
break;
}
} catch( ConfigurationException cex ) {
// OPEN ISSUE: Should we log this? I think so. Convince me otherwise. But maybe
// we should only log it once or every Nth time??
LOGGER.warning(Logger.EVENT_FAILURE, "ESAPI property " +
VALIDATOR_HTML_VALIDATION_ACTION +
" must be set to either \"clean\" (the default for legacy support) or \"throw\"; assuming \"clean\"",
cex);
}
return legacy;
}
private String invokeAntiSamy( String context, String input ) throws ValidationException {
// CHECKME should this allow empty Strings? " " use IsBlank instead?
if ( StringUtilities.isEmpty(input) ) {
if (allowNull) {
return null;
}
throw new ValidationException( context + " is required", "AntiSamy validation error: context=" + context + ", input=" + input, context );
}
String canonical = super.getValid( context, input );
try {
AntiSamy as = new AntiSamy();
CleanResults test = as.scan(canonical, antiSamyPolicy); // Uses AntiSamy.DOM scanner.
List<String> errors = test.getErrorMessages();
if ( !errors.isEmpty() ) {
if ( legacyHtmlValidation() ) { // See GitHub issues 509 and 521
LOGGER.info(Logger.SECURITY_FAILURE, "Cleaned up invalid HTML input: " + errors );
} else {
throw new ValidationException( context + ": Invalid HTML input", "Invalid HTML input does not follow rules in antisamy-esapi.xml: context=" + context + " errors=" + errors.toString());
}
}
return test.getCleanHTML().trim();
} catch (ScanException e) {
throw new ValidationException( context + ": Invalid HTML input", "Invalid HTML input: context=" + context + " error=" + e.getMessage(), e, context );
} catch (PolicyException e) {
throw new ValidationException( context + ": Invalid HTML input", "Invalid HTML input does not follow rules in antisamy-esapi.xml: context=" + context + " error=" + e.getMessage(), e, context );
}
}
}