/* * (C) Copyright 2006-2010 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.platform.htmlsanitizer; import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.PropertyNotFoundException; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; 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; /** * Service that sanitizes some HMTL fields to remove potential cross-site scripting attacks in them. */ public class HtmlSanitizerServiceImpl extends DefaultComponent implements HtmlSanitizerService { private static final Log log = LogFactory.getLog(HtmlSanitizerServiceImpl.class); public static final String ANTISAMY_XP = "antisamy"; public static final String SANITIZER_XP = "sanitizer"; /** All policies registered. */ public LinkedList<HtmlSanitizerAntiSamyDescriptor> allPolicies = new LinkedList<HtmlSanitizerAntiSamyDescriptor>(); /** Effective policy. */ public Policy policy; /** All sanitizers registered. */ public List<HtmlSanitizerDescriptor> allSanitizers = new ArrayList<HtmlSanitizerDescriptor>(1); /** Effective sanitizers. */ public List<HtmlSanitizerDescriptor> sanitizers = new ArrayList<HtmlSanitizerDescriptor>(1); @Override public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (ANTISAMY_XP.equals(extensionPoint)) { if (!(contribution instanceof HtmlSanitizerAntiSamyDescriptor)) { log.error("Contribution " + contribution + " is not of type " + HtmlSanitizerAntiSamyDescriptor.class.getName()); return; } HtmlSanitizerAntiSamyDescriptor desc = (HtmlSanitizerAntiSamyDescriptor) contribution; log.info("Registering AntiSamy policy: " + desc.policy); addAntiSamy(desc); } else if (SANITIZER_XP.equals(extensionPoint)) { if (!(contribution instanceof HtmlSanitizerDescriptor)) { log.error("Contribution " + contribution + " is not of type " + HtmlSanitizerDescriptor.class.getName()); return; } HtmlSanitizerDescriptor desc = (HtmlSanitizerDescriptor) contribution; log.info("Registering HTML sanitizer: " + desc); addSanitizer(desc); } else { log.error("Contribution extension point should be '" + SANITIZER_XP + "' but is: " + extensionPoint); } } @Override public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { if (ANTISAMY_XP.equals(extensionPoint)) { if (!(contribution instanceof HtmlSanitizerAntiSamyDescriptor)) { return; } HtmlSanitizerAntiSamyDescriptor desc = (HtmlSanitizerAntiSamyDescriptor) contribution; log.info("Unregistering AntiSamy policy: " + desc.policy); removeAntiSamy(desc); } else if (SANITIZER_XP.equals(extensionPoint)) { if (!(contribution instanceof HtmlSanitizerDescriptor)) { return; } HtmlSanitizerDescriptor desc = (HtmlSanitizerDescriptor) contribution; log.info("Unregistering HTML sanitizer: " + desc); removeSanitizer(desc); } } protected void addAntiSamy(HtmlSanitizerAntiSamyDescriptor desc) { if (Thread.currentThread().getContextClassLoader().getResourceAsStream(desc.policy) == null) { log.error("Cannot find AntiSamy policy: " + desc.policy); return; } allPolicies.add(desc); refreshPolicy(); } protected void removeAntiSamy(HtmlSanitizerAntiSamyDescriptor desc) { allPolicies.remove(desc); refreshPolicy(); } protected void refreshPolicy() { if (allPolicies.isEmpty()) { policy = null; } else { HtmlSanitizerAntiSamyDescriptor desc = allPolicies.removeLast(); InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(desc.policy); try { policy = Policy.getInstance(is); } catch (PolicyException e) { policy = null; throw new RuntimeException("Cannot parse AntiSamy policy: " + desc.policy, e); } } } protected Policy getPolicy() { return policy; } protected void addSanitizer(HtmlSanitizerDescriptor desc) { if (desc.fields.isEmpty()) { log.error("Sanitizer has no fields: " + desc); return; } allSanitizers.add(desc); refreshSanitizers(); } protected void removeSanitizer(HtmlSanitizerDescriptor desc) { allSanitizers.remove(desc); refreshSanitizers(); } protected void refreshSanitizers() { // not very efficient algorithm but who cares? sanitizers.clear(); for (HtmlSanitizerDescriptor sanitizer : allSanitizers) { // remove existing with same name for (Iterator<HtmlSanitizerDescriptor> it = sanitizers.iterator(); it.hasNext();) { HtmlSanitizerDescriptor s = it.next(); if (s.name.equals(sanitizer.name)) { it.remove(); break; } } // add new one if enabled if (sanitizer.enabled) { sanitizers.add(sanitizer); } } } protected List<HtmlSanitizerDescriptor> getSanitizers() { return sanitizers; } // ----- HtmlSanitizerService ----- @Override public void sanitizeDocument(DocumentModel doc) { if (policy == null) { log.error("Cannot sanitize, no policy registered"); return; } for (HtmlSanitizerDescriptor sanitizer : sanitizers) { if (!sanitizer.types.isEmpty() && !sanitizer.types.contains(doc.getType())) { continue; } for (FieldDescriptor field : sanitizer.fields) { String fieldName = field.getContentField(); String filterField = field.getFilterField(); if (filterField != null) { Property filterProp; try { filterProp = doc.getProperty(filterField); } catch (PropertyNotFoundException e) { continue; } if (field.match(String.valueOf(filterProp.getValue())) != field.doSanitize()) { continue; } } Property prop; try { prop = doc.getProperty(fieldName); } catch (PropertyNotFoundException e) { continue; } Serializable value = prop.getValue(); if (value == null) { continue; } if (!(value instanceof String)) { log.debug("Cannot sanitize non-string field: " + field); continue; } String info = "doc " + doc.getPathAsString() + " (" + doc.getId() + ") field " + field; String newValue = sanitizeString((String) value, info); if (!newValue.equals(value)) { prop.setValue(newValue); } } } } @Override public String sanitizeString(String string, String info) { if (policy == null) { log.error("Cannot sanitize, no policy registered"); return string; } try { CleanResults cr = new AntiSamy().scan(string, policy); for (Object err : cr.getErrorMessages()) { log.debug(String.format("Sanitizing %s: %s", info == null ? "" : info, err)); } return cr.getCleanHTML(); } catch (ScanException | PolicyException e) { log.error(String.format("Cannot sanitize %s: %s", info == null ? "" : info, e)); return string; } } }