001package org.jsoup.safety; 002 003import org.jsoup.helper.Validate; 004import org.jsoup.nodes.Attribute; 005import org.jsoup.nodes.Attributes; 006import org.jsoup.nodes.DataNode; 007import org.jsoup.nodes.Document; 008import org.jsoup.nodes.Element; 009import org.jsoup.nodes.Node; 010import org.jsoup.nodes.NodeInternals; 011import org.jsoup.nodes.Range; 012import org.jsoup.nodes.TextNode; 013import org.jsoup.parser.ParseErrorList; 014import org.jsoup.parser.Parser; 015import org.jsoup.select.NodeVisitor; 016 017import java.util.List; 018 019import static org.jsoup.internal.SharedConstants.DummyUri; 020 021/** 022 The {@link Safelist}-based HTML cleaner. Use to ensure that end-user provided HTML contains only the elements and attributes 023 that you are expecting; no junk, and no cross-site scripting attacks! 024 <p> 025 The HTML cleaner parses the input as HTML and then runs it through a safelist, so the output HTML can only contain 026 HTML that is allowed by the safelist. 027 </p> 028 <p> 029 It is assumed that the input HTML is a body fragment; the clean methods only pull from the source's body, and the 030 canned safelists only allow body-contained tags. 031 </p> 032 <p> 033 Rather than interacting directly with a Cleaner object, generally see the {@code clean} methods in {@link org.jsoup.Jsoup}. 034 </p> 035 <p> 036 A Cleaner may be reused across multiple documents and shared across concurrent threads once its {@link Safelist} has 037 been configured. The cleaner uses the supplied safelist directly, so later safelist changes affect later cleaning 038 calls. If you need a variant of an existing configuration, use {@link Safelist#Safelist(Safelist)} to make a copy. 039 </p> 040 */ 041public class Cleaner { 042 private final Safelist safelist; 043 044 /** 045 Create a new cleaner, that sanitizes documents using the supplied safelist. 046 @param safelist safe-list to clean with 047 */ 048 public Cleaner(Safelist safelist) { 049 Validate.notNull(safelist); 050 this.safelist = safelist; 051 } 052 053 /** 054 Creates a new, clean document, from the original dirty document, containing only elements allowed by the safelist. 055 The original document is not modified. Only elements from the dirty document's <code>body</code> are used. The 056 OutputSettings of the original document are cloned into the clean document. 057 @param dirtyDocument Untrusted base document to clean. 058 @return cleaned document. 059 */ 060 public Document clean(Document dirtyDocument) { 061 Validate.notNull(dirtyDocument); 062 063 Document clean = Document.createShell(dirtyDocument.baseUri()); 064 copySafeNodes(dirtyDocument.body(), clean.body()); 065 clean.outputSettings(dirtyDocument.outputSettings().clone()); 066 067 return clean; 068 } 069 070 /** 071 Determines if the input document's <b>body</b> is valid, against the safelist. It is considered valid if all the 072 tags and attributes in the input HTML are allowed by the safelist, and that there is no content in the 073 <code>head</code>. 074 <p> 075 This method is intended to be used in a user interface as a validator for user input. Note that regardless of the 076 output of this method, the input document <b>must always</b> be normalized using a method such as 077 {@link #clean(Document)}, and the result of that method used to store or serialize the document before later reuse 078 such as presentation to end users. This ensures that enforced attributes are set correctly, and that any 079 differences between how a given browser and how jsoup parses the input HTML are normalized. 080 </p> 081 <p>Example: 082 <pre>{@code 083 Document inputDoc = Jsoup.parse(inputHtml); 084 Cleaner cleaner = new Cleaner(Safelist.relaxed()); 085 boolean isValid = cleaner.isValid(inputDoc); 086 Document normalizedDoc = cleaner.clean(inputDoc); 087 }</pre> 088 </p> 089 @param dirtyDocument document to test 090 @return true if no tags or attributes need to be removed; false if they do 091 */ 092 public boolean isValid(Document dirtyDocument) { 093 Validate.notNull(dirtyDocument); 094 095 Document clean = Document.createShell(dirtyDocument.baseUri()); 096 int numDiscarded = copySafeNodes(dirtyDocument.body(), clean.body()); 097 return numDiscarded == 0 098 && dirtyDocument.head().childNodes().isEmpty(); // because we only look at the body, but we start from a shell, make sure there's nothing in the head 099 } 100 101 /** 102 Determines if the input document's <b>body HTML</b> is valid, against the safelist. It is considered valid if all 103 the tags and attributes in the input HTML are allowed by the safelist. 104 <p> 105 This method is intended to be used in a user interface as a validator for user input. Note that regardless of the 106 output of this method, the input document <b>must always</b> be normalized using a method such as 107 {@link #clean(Document)}, and the result of that method used to store or serialize the document before later reuse 108 such as presentation to end users. This ensures that enforced attributes are set correctly, and that any 109 differences between how a given browser and how jsoup parses the input HTML are normalized. 110 </p> 111 <p>Example: 112 <pre>{@code 113 Document inputDoc = Jsoup.parse(inputHtml); 114 Cleaner cleaner = new Cleaner(Safelist.relaxed()); 115 boolean isValid = cleaner.isValidBodyHtml(inputHtml); 116 Document normalizedDoc = cleaner.clean(inputDoc); 117 }</pre> 118 </p> 119 @param bodyHtml HTML fragment to test 120 @return true if no tags or attributes need to be removed; false if they do 121 */ 122 public boolean isValidBodyHtml(String bodyHtml) { 123 String baseUri = (safelist.preserveRelativeLinks()) ? DummyUri : ""; // fake base URI to allow relative URLs to remain valid 124 Document clean = Document.createShell(baseUri); 125 Document dirty = Document.createShell(baseUri); 126 ParseErrorList errorList = ParseErrorList.tracking(1); 127 List<Node> nodes = Parser.parseFragment(bodyHtml, dirty.body(), baseUri, errorList); 128 dirty.body().insertChildren(0, nodes); 129 int numDiscarded = copySafeNodes(dirty.body(), clean.body()); 130 return numDiscarded == 0 && errorList.isEmpty(); 131 } 132 133 /** 134 Iterates the input and copies trusted nodes (tags, attributes, text) into the destination. 135 */ 136 private final class CleaningVisitor implements NodeVisitor { 137 private int numDiscarded = 0; 138 private final Element root; 139 private Element destination; // current element to append nodes to 140 141 private CleaningVisitor(Element root, Element destination) { 142 this.root = root; 143 this.destination = destination; 144 } 145 146 @Override public void head(Node source, int depth) { 147 if (source instanceof Element) { 148 Element sourceEl = (Element) source; 149 150 if (safelist.isSafeTag(sourceEl.normalName())) { // safe, clone and copy safe attrs 151 ElementMeta meta = createSafeElement(sourceEl); 152 Element destChild = meta.el; 153 destination.appendChild(destChild); 154 155 numDiscarded += meta.numAttribsDiscarded; 156 destination = destChild; 157 } else if (source != root) { // not a safe tag, so don't add. don't count root against discarded. 158 numDiscarded++; 159 } 160 } else if (source instanceof TextNode) { 161 TextNode sourceText = (TextNode) source; 162 TextNode destText = new TextNode(sourceText.getWholeText()); 163 destination.appendChild(destText); 164 } else if (source instanceof DataNode && safelist.isSafeTag(source.parent().normalName())) { 165 DataNode sourceData = (DataNode) source; 166 DataNode destData = new DataNode(sourceData.getWholeData()); 167 destination.appendChild(destData); 168 } else { // else, we don't care about comments, xml proc instructions, etc 169 numDiscarded++; 170 } 171 } 172 173 @Override public void tail(Node source, int depth) { 174 if (source instanceof Element && safelist.isSafeTag(source.normalName())) { 175 destination = destination.parent(); // would have descended, so pop destination stack 176 } 177 } 178 } 179 180 private int copySafeNodes(Element source, Element dest) { 181 CleaningVisitor cleaningVisitor = new CleaningVisitor(source, dest); 182 cleaningVisitor.traverse(source); 183 return cleaningVisitor.numDiscarded; 184 } 185 186 private ElementMeta createSafeElement(Element sourceEl) { 187 Element dest = sourceEl.shallowClone(); // reuses tag, clones attributes and preserves any user data 188 String sourceTag = sourceEl.tagName(); 189 Attributes destAttrs = dest.attributes(); 190 dest.clearAttributes(); // clear all non-internal attributes, ready for safe copy 191 192 int numDiscarded = 0; 193 Attributes sourceAttrs = sourceEl.attributes(); 194 for (Attribute sourceAttr : sourceAttrs) { 195 if (safelist.isSafeAttribute(sourceTag, sourceEl, sourceAttr)) { // will keep this attr 196 String key = sourceAttr.getKey(); 197 String value = sourceAttr.getValue(); 198 199 if (safelist.shouldAbsUrl(sourceTag, key)) { // configured to make absolute urls for this key (href) 200 value = sourceEl.absUrl(key); 201 if (value.isEmpty()) // could not be made abs; leave as-is to allow custom unknown protocols 202 value = sourceAttr.getValue(); 203 } 204 Range.AttributeRange range = sourceAttrs.sourceRange(key); 205 destAttrs.put(key, value); 206 NodeInternals.attributeRange(destAttrs, key, range); 207 } else 208 numDiscarded++; 209 } 210 211 212 Attributes enforcedAttrs = safelist.getEnforcedAttributes(sourceTag); 213 // special case for <a href rel=nofollow>, only apply to external links: 214 if (sourceEl.nameIs("a") && enforcedAttrs.get("rel").equals("nofollow")) { 215 String href = sourceEl.absUrl("href"); 216 String sourceBase = sourceEl.baseUri(); 217 if (!href.isEmpty() && !sourceBase.isEmpty() && href.startsWith(sourceBase)) { // same site, so don't set the nofollow 218 enforcedAttrs.remove("rel"); 219 } 220 } 221 222 // apply enforced attributes case-insensitively, so a preserved-case source attr is canonicalized to the enforced key 223 for (Attribute enforcedAttr : enforcedAttrs) { 224 destAttrs.removeIgnoreCase(enforcedAttr.getKey()); 225 destAttrs.put(enforcedAttr.getKey(), enforcedAttr.getValue()); 226 } 227 dest.attributes().addAll(destAttrs); // re-attach, if removed in clear 228 return new ElementMeta(dest, numDiscarded); 229 } 230 231 private static class ElementMeta { 232 Element el; 233 int numAttribsDiscarded; 234 235 ElementMeta(Element el, int numAttribsDiscarded) { 236 this.el = el; 237 this.numAttribsDiscarded = numAttribsDiscarded; 238 } 239 } 240 241}