/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.util.filter.impl;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import org.olat.core.logging.OLATRuntimeException;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.filter.Filter;
import org.olat.core.util.vfs.VFSManager;
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;
/**
* Description:<br>
* OWASP AntiSamy XSSFilter
* creates a DOM-Tree, parses it and filters everything invalid out, expect items in the policy-file
*
* this is way better than trying to handle input by regexp's:
* http://htmlparsing.icenine.ca/doku.php/#summary
*
* OWASP AntiSamy docu: see http://www.owasp.org/index.php/AntiSamy
* HTML Parser based on: http://nekohtml.sourceforge.net/
*
* <P>
* Initial Date: 30.07.2009 <br>
* @author Roman Haag, roman.haag@frentix.com
*/
public class OWASPAntiSamyXSSFilter implements Filter {
private static final OLog log = Tracing.createLoggerFor(OWASPAntiSamyXSSFilter.class);
//to be found in /_resources
private static final String POLICY_FILE = "antisamy-tinymce.xml";
private static boolean jUnitDebug;
private CleanResults cr;
private final int maxLength;
private final boolean entityEncodeIntlChars;
private static Policy tinyMcePolicy;
private static Policy internalionalTinyMcePolicy;
static {
try {
String fPath = VFSManager.sanitizePath(OWASPAntiSamyXSSFilter.class.getPackage().getName());
fPath = fPath.replace('.', '/');
fPath = fPath + "/_resources/" + POLICY_FILE;
InputStream inStream = OWASPAntiSamyXSSFilter.class.getResourceAsStream(fPath);
tinyMcePolicy = Policy.getInstance(inStream);
internalionalTinyMcePolicy = tinyMcePolicy.cloneWithDirective("entityEncodeIntlChars", "false");
} catch (Exception e) {
log.error("", e);
}
}
public OWASPAntiSamyXSSFilter(){
this(-1, true, false);
}
/**
* @param maxLength
* @param junitDebug
*/
public OWASPAntiSamyXSSFilter(int maxLength, boolean junitDebug){
this(maxLength, true, junitDebug);
}
public OWASPAntiSamyXSSFilter(int maxLength, boolean entityEncodeIntlChars, boolean junitDebug){
OWASPAntiSamyXSSFilter.jUnitDebug = junitDebug;
this.maxLength = maxLength;
this.entityEncodeIntlChars = entityEncodeIntlChars;
}
/**
* @see org.olat.core.util.filter.Filter#filter(java.lang.String)
*/
public String filter(String original) {
if (jUnitDebug) System.out.println("************************************************");
if (jUnitDebug) System.out.println(" Input: " + original);
if (original == null) {
if (log.isDebug()) log.debug(" Filter-Input was null, is this intended?", null);
return null;
}
String output = getCleanHTML(original);
if (original.equals(output)) {
// logInfo(" filter worked correctly!", null);
} else {
String errMsg = getOrPrintErrorMessages();
if (!errMsg.equals("")) {
log.warn(" Filter applied! => message from filter, check if this should not be allowed: " + errMsg, null);
log.info(" Original Input: \n" + original, null);
log.info(" Filter Result: \n" + output, null);
} else {
log.debug(" Filter result doesn't match input! / no message from filter! maybe only some formatting differences.", null);
}
}
return output;
}
private void printOriginStackTrace() {
// use stacktrace to find out more where the filter was used
OLATRuntimeException ore = new OLATRuntimeException("XSSFilter dummy", null);
final Writer result = new StringWriter();
final PrintWriter printWriter = new PrintWriter(result);
ore.printStackTrace(printWriter);
}
private String getCleanHTML(String original) {
Policy policy;
if(entityEncodeIntlChars) {
policy = tinyMcePolicy;
} else {
policy = internalionalTinyMcePolicy;
}
if(maxLength > 0) {
policy = policy.cloneWithDirective("maxInputSize", Integer.toString(maxLength));
}
AntiSamy as = new AntiSamy();
cr = null;
try {
cr = as.scan(original, policy);
} catch (ScanException e) {
log.error("XSS Filter scan error", e);
printOriginStackTrace();
} catch (PolicyException e) {
log.error("XSS Filter policy error", e);
printOriginStackTrace();
}
String output;
try {
output = cr.getCleanHTML();
} catch (Exception | Error e){
output = "";
log.error("Error getting cleaned HTML from string::" + original, e);
}
if (jUnitDebug) System.out.println("OWASP-AntiSamy-Outp: " + output);
getOrPrintErrorMessages();
if (jUnitDebug) System.out.println("OWASP-ParseTime: " + cr.getScanTime());
return output;
}
public int getNumOfErrors() {
if (cr != null) {
return cr.getNumberOfErrors();
}
return -1;
}
/**
* get Errors/Messages from filter.
* This have not to be "errors", its what has been filtered and gets reported.
* @return
*/
public String getOrPrintErrorMessages(){
String errors = "";
if (cr!=null){
if (cr.getNumberOfErrors()!=0) {
errors = "OWASP-Errors: " + cr.getErrorMessages();
if (jUnitDebug) System.out.println(errors);
}
}
return errors;
}
}