From 8dfb49b5664fa614465626c2cb48bb213ad57e90 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sun, 28 Jul 2013 17:53:17 -0400 Subject: [PATCH] Fix #104 and #112: Allow people to override content filter blacklist/whitelist --- README.markdown | 52 +++++++++++++++++++++- index.php | 9 +--- vendor/PicoFeed/Filter.php | 88 +++++++++++++++++++++++--------------- 3 files changed, 105 insertions(+), 44 deletions(-) diff --git a/README.markdown b/README.markdown index 4f163b5..c49cfd1 100644 --- a/README.markdown +++ b/README.markdown @@ -161,13 +161,13 @@ To override them, create a `config.php` file at the root of the project and chan By example, to override the default HTTP timeout value: - # file config.php - default value is 10 seconds @@ -189,6 +189,54 @@ you can save sessions in a custom directory. - Override the application variable like described above: `define('SESSION_SAVE_PATH', 'sessions');` - Now, your sessions are saved in the directory `sessions` +### How to override/extends the content filtering blacklist/whitelist? + +Miniflux use [PicoFeed](https://github.com/fguillot/picoFeed) to parse the content of each item. +These variables are public static arrays, extends the actual array or replace it. + +**Be careful, you can break everything by doing that!!!** + +Put your modifications in your custom `config.php` like described above. + +By example to add a new iframe whitelist: + + \PicoFeed\Filter::$iframe_whitelist[] = 'http://www.kickstarter.com'; + +Or to replace the entire whitelist: + + \PicoFeed\Filter::$iframe_whitelist = array('http://www.kickstarter.com'); + +Available variables: + + // Allow only specified tags and attributes + \PicoFeed\Filter::$whitelist_tags + + // Strip content of these tags + \PicoFeed\Filter::$blacklist_tags + + // Allow only specified URI scheme + \PicoFeed\Filter::$whitelist_scheme + + // List of attributes used for external resources: src and href + \PicoFeed\Filter::$media_attributes + + // Blacklist of external resources + \PicoFeed\Filter::$media_blacklist + + // Required attributes for tags, if the attribute is missing the tag is dropped + \PicoFeed\Filter::$required_attributes + + // Add attribute to specified tags + \PicoFeed\Filter::$add_attributes + + // Attributes that must be integer + \PicoFeed\Filter::$integer_attributes + + // Iframe allowed source + \PicoFeed\Filter::$iframe_whitelist + +For more details, have a look to the class `vendor/PicoFeed/Filter.php`. + ### How to create a theme for Miniflux? It's very easy to write a custom theme for Miniflux. diff --git a/index.php b/index.php index 4f2246b..293d3f2 100644 --- a/index.php +++ b/index.php @@ -36,14 +36,7 @@ Router\before(function($action) { Response\csp(array( 'media-src' => '*', 'img-src' => '*', - 'frame-src' => implode(' ', array( - 'http://www.youtube.com', - 'https://www.youtube.com', - 'http://player.vimeo.com', - 'https://player.vimeo.com', - 'http://www.dailymotion.com', - 'https://www.dailymotion.com', - )) + 'frame-src' => implode(' ', \PicoFeed\Filter::$iframe_whitelist) )); Response\xframe(); diff --git a/vendor/PicoFeed/Filter.php b/vendor/PicoFeed/Filter.php index 82f87f2..d4012b6 100644 --- a/vendor/PicoFeed/Filter.php +++ b/vendor/PicoFeed/Filter.php @@ -10,7 +10,8 @@ class Filter private $empty_tags = array(); private $strip_content = false; - public $allowed_tags = array( + // Allow only these tags and attributes + public static $whitelist_tags = array( 'audio' => array('controls', 'src'), 'video' => array('poster', 'controls', 'height', 'width', 'src'), 'source' => array('src', 'type'), @@ -51,12 +52,14 @@ class Filter 'q' => array('cite') ); - public $strip_tags_content = array( + // Strip content of these tags + public static $blacklist_tags = array( 'script' ); - // http://en.wikipedia.org/wiki/URI_scheme - public $allowed_protocols = array( + // Allowed URI scheme + // For a complete list go to http://en.wikipedia.org/wiki/URI_scheme + public static $scheme_whitelist = array( '//', 'data:image/png;base64,', 'data:image/gif;base64,', @@ -92,12 +95,15 @@ class Filter 'tel:', ); - public $protocol_attributes = array( + // Attributes used for external resources + public static $media_attributes = array( 'src', 'href', + 'poster', ); - public $blacklist_media = array( + // Blacklisted resources + public static $media_blacklist = array( 'feeds.feedburner.com', 'share.feedsportal.com', 'da.feedsportal.com', @@ -119,20 +125,32 @@ class Filter 'plus.google.com/share', 'www.gstatic.com/images/icons/gplus-16.png', 'www.gstatic.com/images/icons/gplus-32.png', - 'www.gstatic.com/images/icons/gplus-64.png' + 'www.gstatic.com/images/icons/gplus-64.png', ); - public $required_attributes = array( + // Mandatory attributes for specified tags + public static $required_attributes = array( 'a' => array('href'), 'img' => array('src'), - 'iframe' => array('src') + 'iframe' => array('src'), + 'audio' => array('src'), + 'source' => array('src'), ); - public $add_attributes = array( + // Add attributes to specified tags + public static $add_attributes = array( 'a' => 'rel="noreferrer" target="_blank"' ); - public $iframe_allowed_resources = array( + // Attributes that must be integer + public static $integer_attributes = array( + 'width', + 'height', + 'frameborder', + ); + + // Iframe source whitelist, everything else is ignored + public static $iframe_whitelist = array( '//www.youtube.com', 'http://www.youtube.com/', 'https://www.youtube.com/', @@ -218,12 +236,12 @@ class Filter $attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"'; $used_attributes[] = $attribute; } - else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistMedia($value)) { + else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistedMedia($value)) { if ($attribute == 'src' && isset($attributes['data-src']) && $this->isAllowedProtocol($attributes['data-src']) && - ! $this->isBlacklistMedia($attributes['data-src'])) { + ! $this->isBlacklistedMedia($attributes['data-src'])) { $value = $attributes['data-src']; } @@ -232,7 +250,7 @@ class Filter $used_attributes[] = $attribute; } } - else { + else if ($this->validateAttributeValue($attribute, $value)) { $attr_data .= ' '.$attribute.'="'.$value.'"'; $used_attributes[] = $attribute; @@ -241,9 +259,9 @@ class Filter } // Check for required attributes - if (isset($this->required_attributes[$name])) { + if (isset(self::$required_attributes[$name])) { - foreach ($this->required_attributes[$name] as $required_attribute) { + foreach (self::$required_attributes[$name] as $required_attribute) { if (! in_array($required_attribute, $used_attributes)) { @@ -258,9 +276,9 @@ class Filter $this->data .= '<'.$name.$attr_data; // Add custom attributes - if (isset($this->add_attributes[$name])) { + if (isset(self::$add_attributes[$name])) { - $this->data .= ' '.$this->add_attributes[$name].' '; + $this->data .= ' '.self::$add_attributes[$name].' '; } // If img or br, we don't close it here @@ -268,7 +286,7 @@ class Filter } } - if (in_array($name, $this->strip_tags_content)) { + if (in_array($name, self::$blacklist_tags)) { $this->strip_content = true; } @@ -294,8 +312,6 @@ class Filter public function getAbsoluteUrl($path, $url) { - //if (! filter_var($url, FILTER_VALIDATE_URL)) return ''; - $components = parse_url($url); if (! isset($components['scheme'])) $components['scheme'] = 'http'; @@ -325,12 +341,10 @@ class Filter $length = strlen($url_path); if ($length > 1 && $url_path{$length - 1} !== '/') { - $url_path = dirname($url_path).'/'; } if (substr($path, 0, 2) === './') { - $path = substr($path, 2); } @@ -342,35 +356,33 @@ class Filter public function isRelativePath($value) { if (strpos($value, 'data:') === 0) return false; - return strpos($value, '://') === false && strpos($value, '//') !== 0; } public function isAllowedTag($name) { - return isset($this->allowed_tags[$name]); + return isset(self::$whitelist_tags[$name]); } public function isAllowedAttribute($tag, $attribute) { - return in_array($attribute, $this->allowed_tags[$tag]); + return in_array($attribute, self::$whitelist_tags[$tag]); } public function isResource($attribute) { - return in_array($attribute, $this->protocol_attributes); + return in_array($attribute, self::$media_attributes); } public function isAllowedIframeResource($value) { - foreach ($this->iframe_allowed_resources as $url) { + foreach (self::$iframe_whitelist as $url) { if (strpos($value, $url) === 0) { - return true; } } @@ -381,10 +393,9 @@ class Filter public function isAllowedProtocol($value) { - foreach ($this->allowed_protocols as $protocol) { + foreach (self::$scheme_whitelist as $protocol) { if (strpos($value, $protocol) === 0) { - return true; } } @@ -393,12 +404,11 @@ class Filter } - public function isBlacklistMedia($resource) + public function isBlacklistedMedia($resource) { - foreach ($this->blacklist_media as $name) { + foreach (self::$media_blacklist as $name) { if (strpos($resource, $name) !== false) { - return true; } } @@ -413,4 +423,14 @@ class Filter isset($attributes['height']) && isset($attributes['width']) && $attributes['height'] == 1 && $attributes['width'] == 1; } + + + public function validateAttributeValue($attribute, $value) + { + if (in_array($attribute, self::$integer_attributes)) { + return ctype_digit($value); + } + + return true; + } }