<?php /* * Plugin Name: A/I - Sanitize Custom CSS * Description: Sanitize Custom CSS by preventing url() references * Version: 0.0.1 * Author: Autistici/Inventati * Author URI: https://autistici.org */ /* * This plugin sanitizes the custom CSS that admins can add to their * websites, by removing constructs referencing URLs that can lead to * XSS (@import, url refs, etc). The point of it is to allow all * admins on a multisite instance to edit CSS (not just super-admins), * via something like the multisite-custom-css plugin. * * It uses CSSTidy to parse the custom CSS, then inspects the CSS * structure and removes unwanted directives, before finally rendering * the CSS again. It does so by overriding some methods in our * "safecss" custom subclass of the CSSTidy optimizer. * * The custom CSS hook structure that invokes CSSTidy is lifted from * the Jetpack plugin, modules/custom-css/custom-css-4.7.php, with the * sanitization part taken from * https://meta.trac.wordpress.org/changeset/2085. */ function ai_sanitize_urls_in_css_properties($url, $property) { $property = trim($property); $url = trim($url, "' \" \r \n"); // The current policy is: no URLs at all. return ''; } // Lazy load csstidy and our custom subclasses. function ai_safecss_class() { if (class_exists('safecss')) { return; } require_once(__DIR__ . '/csstidy/class.csstidy.php'); class safecss extends csstidy_optimise { public function subvalue() { $this->sub_value = trim($this->sub_value); if (preg_match('!^\s*(?P<url_expression>url\s*(?P<opening_paren>\(|\\0028)(?P<parenthetical_content>.*)(?P<closing_paren>\)|\\0029))(.*)$!Dis', $this->sub_value, $matches)) { $this->sub_value = ai_sanitize_urls_in_css_properties($matches['parenthetical_content'], $this->property); // Only replace the url([...]) portion of the sub_value so we don't // lose things like trailing commas or !important declarations. if ($this->sub_value) { $this->sub_value = str_replace($matches['url_expression'], $this->sub_value, $matches[0]); } } // Strip any expressions if (preg_match('!^\\s*expression!Dis', $this->sub_value)) { $this->sub_value = ''; } return parent::subvalue(); } public function postparse() { // @import rules are stripped because they can enable XSS. $this->parser->import = array(); // @charset rules are stripped because manipulating the charset // can allow an attacker to introduce XSS vulnerabilities by // tricking the browser into interpreting the CSS as HTML. $this->parser->charset = array(); return parent::postparse(); } } } function ai_sanitize_css($css, $args = array()) { $args = wp_parse_args( $args, array( 'force' => false, ) ); if ($args['force'] || !current_user_can('unfiltered_html')) { $warnings = array(); ai_safecss_class(); $csstidy = new csstidy(); $csstidy->optimise = new safecss( $csstidy ); $csstidy->set_cfg( 'remove_bslash', false ); $csstidy->set_cfg( 'compress_colors', false ); $csstidy->set_cfg( 'compress_font-weight', false ); $csstidy->set_cfg( 'optimise_shorthands', 0 ); $csstidy->set_cfg( 'remove_last_;', false ); $csstidy->set_cfg( 'case_properties', false ); $csstidy->set_cfg( 'discard_invalid_properties', true ); $csstidy->set_cfg( 'css_level', 'CSS3.0' ); $csstidy->set_cfg( 'preserve_css', true ); $csstidy->set_cfg( 'template', __DIR__ . '/csstidy/wordpress-standard.tpl' ); // Test for some preg_replace stuff. $prev = $css; $css = preg_replace( '/\\\\([0-9a-fA-F]{4})/', '\\\\\\\\$1', $css ); // prevent content: '\3434' from turning into '\\3434'. $css = str_replace( array( '\'\\\\', '"\\\\' ), array( '\'\\', '"\\' ), $css ); if ( $css !== $prev ) { $warnings[] = 'preg_replace found stuff'; } // Some people put weird stuff in their CSS, KSES tends to be greedy. $css = str_replace( '<=', '<=', $css ); // Test for some kses stuff. $prev = $css; // Why KSES instead of strip_tags? Who knows? $css = wp_kses_split( $css, array(), array() ); $css = str_replace( '>', '>', $css ); // kses replaces lone '>' with > // Why both KSES and strip_tags? Because we just added some '>'. $css = strip_tags( $css ); if ( $css !== $prev ) { $warnings[] = 'kses found stuff'; } $csstidy->parse( $css ); $css = $csstidy->print->plain(); } return $css; } add_filter('update_custom_css_data', function($args) { $css = $args['css']; $args['css'] = ai_sanitize_css($css); return $args; }, 10, 2);