Have I missed some obvious XSS vulnerabilities? As the question states: have I missed some obvious security vulnerabilities in this code? Particularly interested in XSS...

I'm trying to create something more robust that strip_tags, but simpler than HTML Purifier which just feels like overkill.

class StripTags {
    private $tags;
    private $dom;
    function __construct($html, $tags = null) {
        // Setup tags
        $this-&gt;setTags($tags);

        //Initialise document using provided HTML
        $doc = new DOMDocument();
        @$doc-&gt;loadHTML($html);         //suppress invalid HTML warnings
        $doc_elem = $doc-&gt;documentElement;

        $this-&gt;traverseAndRemove($doc_elem);

        // Store DOM for processing in __toString()
        $this-&gt;dom = $doc;
    }
    public function __toString() {
        // Remove unwanted DOCTYPE etc
        $str = $this-&gt;dom-&gt;saveHTML();
        $str = str_replace('&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;', "", $str);
        $str = preg_replace("/^&lt;html&gt;&lt;body&gt;/", "", trim($str));
        $str = preg_replace("/&lt;\/body&gt;&lt;\/html&gt;$/", "", trim($str));
        return $str;
    }

    private function setTags($t) {
        if (empty($t) or !is_array($t)) {
            $this-&gt;tags = array("html", "body", "p", "span", "div", "img", "b", "i", "em", "strong", "h1", "h2", "h3", "h4", "a", "pre");
        } else {
            $this-&gt;tags = $t;
        }
        if (!in_array("html",$this-&gt;tags)) {
            $this-&gt;tags[] = "html";
        }
        if (!in_array("body",$this-&gt;tags)) {
            $this-&gt;tags[] = "body";
        }
    }

    private function traverseAndRemove(&amp;$elem) {
        // Check node type, attributes and child nodes
        // Reset child/attribute loops if one is removed
        if ($elem-&gt;nodeType === XML_ELEMENT_NODE) {
            $tagName = $elem-&gt;tagName;
            if (!in_array($tagName, $this-&gt;tags)) {
                // Remove any elements in the tags array and stop processing it
                $elem-&gt;parentNode-&gt;removeChild($elem);
                return;
            }
            // Check attributes for Javascript events and src/hrefs with javascript too...
            if ($elem-&gt;hasAttributes()) {
                $attrs = $elem-&gt;attributes;
                for ($i=0,$max=$attrs-&gt;length; $i&lt;$max; $i++) {
                    $name = $attrs-&gt;item($i)-&gt;name;
                    if (in_array($name, array("onload", "onclick","onmouseover","onmousemove","onmousehover","onmousedown","onmouseup", "onunload"))) {
                        $elem-&gt;removeAttribute($name);
                        $max--;
                        $i--;
                    }
                    if (in_array($name,array("src", "href")) and preg_match("/^javascript:/i", $attrs-&gt;item($i)-&gt;value)) {
                        $elem-&gt;removeAttribute($name);
                        $max--;
                        $i--;
                    }
                }
            }
        }
        // Check all child nodes too
        if ($elem-&gt;hasChildNodes()) {
            $children = $elem-&gt;childNodes;
            for ($i=0, $max=$children-&gt;length; $i&lt;$max; $i++) {
                $this-&gt;traverseAndRemove($children-&gt;item($i));
                if ($children-&gt;length &lt; $max) {
                    $i -= ($max - $children-&gt;length);
                    $max = $children-&gt;length;
                }
            }
        }
    }
}
It would help if you explained exactly why you think this approach is more robust than using strip_tags. It will certainly be likely to be a lot slower.

From a quick inspection it doesn't seem to filter CSS behaviours nor data URIs.

If it were me I'd be building on to strip_tags rather than trying to start from scratch.