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}