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