Skip to content
Snippets Groups Projects
Commit 7bffbbdd authored by ale's avatar ale
Browse files

Extend the custom-css plugin to support sanitization

Sanitize CSS by removing URLs and @import / @charset directives.
parent 0c1fa050
Branches
Tags v0.4.1
No related merge requests found
Pipeline #32069 passed
...@@ -7,10 +7,77 @@ ...@@ -7,10 +7,77 @@
* Author URI: https://autistici.org * Author URI: https://autistici.org
*/ */
// The following code is heavily inspired by the Jetpack plugin custom-css module. /*
function ai_sanitize_css( $css, $args = array() ) { * 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'); 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', $safecss->sub_value, $matches)) {
$this->sub_value = ai_sanitize_urls_in_css_properties($matches['parenthetical_content'], $safecss->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 = wp_parse_args(
$args, $args,
array( array(
...@@ -20,9 +87,9 @@ function ai_sanitize_css( $css, $args = array() ) { ...@@ -20,9 +87,9 @@ function ai_sanitize_css( $css, $args = array() ) {
if ($args['force'] || !current_user_can('unfiltered_html')) { if ($args['force'] || !current_user_can('unfiltered_html')) {
$warnings = array(); $warnings = array();
safecss_class(); ai_safecss_class();
$csstidy = new csstidy(); $csstidy = new csstidy();
$csstidy->optimise = new safecss( $csstidy ); $csstidy->optimise = new safecss( $csstidy );
$csstidy->set_cfg( 'remove_bslash', false ); $csstidy->set_cfg( 'remove_bslash', false );
...@@ -54,7 +121,7 @@ function ai_sanitize_css( $css, $args = array() ) { ...@@ -54,7 +121,7 @@ function ai_sanitize_css( $css, $args = array() ) {
$css = wp_kses_split( $css, array(), array() ); $css = wp_kses_split( $css, array(), array() );
$css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt; $css = str_replace( '&gt;', '>', $css ); // kses replaces lone '>' with &gt;
// Why both KSES and strip_tags? Because we just added some '>'. // Why both KSES and strip_tags? Because we just added some '>'.
$css = strip_tags( $css ); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- scared to update this to wp_strip_all_tags since we're building a CSS file here. $css = strip_tags( $css );
if ( $css !== $prev ) { if ( $css !== $prev ) {
$warnings[] = 'kses found stuff'; $warnings[] = 'kses found stuff';
...@@ -63,6 +130,7 @@ function ai_sanitize_css( $css, $args = array() ) { ...@@ -63,6 +130,7 @@ function ai_sanitize_css( $css, $args = array() ) {
$csstidy->parse( $css ); $csstidy->parse( $css );
$css = $csstidy->print->plain(); $css = $csstidy->print->plain();
} }
return $css; return $css;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment