Fix #104 and #112: Allow people to override content filter blacklist/whitelist

This commit is contained in:
Frederic Guillot 2013-07-28 17:53:17 -04:00
parent 3836018c66
commit 8dfb49b566
3 changed files with 105 additions and 44 deletions

View File

@ -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: By example, to override the default HTTP timeout value:
# file config.php
<?php <?php
// My specific HTTP timeout (5 seconds) // My specific HTTP timeout (5 seconds)
define('HTTP_TIMEOUT', 5); define('HTTP_TIMEOUT', 5);
PS: This file must be a PHP file (nothing before the open tag `<?php`).
Actually, the following constants can be overrided: Actually, the following constants can be overrided:
- `HTTP_TIMEOUT` => default value is 10 seconds - `HTTP_TIMEOUT` => 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');` - Override the application variable like described above: `define('SESSION_SAVE_PATH', 'sessions');`
- Now, your sessions are saved in the directory `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? ### How to create a theme for Miniflux?
It's very easy to write a custom theme for Miniflux. It's very easy to write a custom theme for Miniflux.

View File

@ -36,14 +36,7 @@ Router\before(function($action) {
Response\csp(array( Response\csp(array(
'media-src' => '*', 'media-src' => '*',
'img-src' => '*', 'img-src' => '*',
'frame-src' => implode(' ', array( 'frame-src' => implode(' ', \PicoFeed\Filter::$iframe_whitelist)
'http://www.youtube.com',
'https://www.youtube.com',
'http://player.vimeo.com',
'https://player.vimeo.com',
'http://www.dailymotion.com',
'https://www.dailymotion.com',
))
)); ));
Response\xframe(); Response\xframe();

View File

@ -10,7 +10,8 @@ class Filter
private $empty_tags = array(); private $empty_tags = array();
private $strip_content = false; private $strip_content = false;
public $allowed_tags = array( // Allow only these tags and attributes
public static $whitelist_tags = array(
'audio' => array('controls', 'src'), 'audio' => array('controls', 'src'),
'video' => array('poster', 'controls', 'height', 'width', 'src'), 'video' => array('poster', 'controls', 'height', 'width', 'src'),
'source' => array('src', 'type'), 'source' => array('src', 'type'),
@ -51,12 +52,14 @@ class Filter
'q' => array('cite') 'q' => array('cite')
); );
public $strip_tags_content = array( // Strip content of these tags
public static $blacklist_tags = array(
'script' 'script'
); );
// http://en.wikipedia.org/wiki/URI_scheme // Allowed URI scheme
public $allowed_protocols = array( // For a complete list go to http://en.wikipedia.org/wiki/URI_scheme
public static $scheme_whitelist = array(
'//', '//',
'data:image/png;base64,', 'data:image/png;base64,',
'data:image/gif;base64,', 'data:image/gif;base64,',
@ -92,12 +95,15 @@ class Filter
'tel:', 'tel:',
); );
public $protocol_attributes = array( // Attributes used for external resources
public static $media_attributes = array(
'src', 'src',
'href', 'href',
'poster',
); );
public $blacklist_media = array( // Blacklisted resources
public static $media_blacklist = array(
'feeds.feedburner.com', 'feeds.feedburner.com',
'share.feedsportal.com', 'share.feedsportal.com',
'da.feedsportal.com', 'da.feedsportal.com',
@ -119,20 +125,32 @@ class Filter
'plus.google.com/share', 'plus.google.com/share',
'www.gstatic.com/images/icons/gplus-16.png', 'www.gstatic.com/images/icons/gplus-16.png',
'www.gstatic.com/images/icons/gplus-32.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'), 'a' => array('href'),
'img' => array('src'), '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"' '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', '//www.youtube.com',
'http://www.youtube.com/', 'http://www.youtube.com/',
'https://www.youtube.com/', 'https://www.youtube.com/',
@ -218,12 +236,12 @@ class Filter
$attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"'; $attr_data .= ' '.$attribute.'="'.$this->getAbsoluteUrl($value, $this->url).'"';
$used_attributes[] = $attribute; $used_attributes[] = $attribute;
} }
else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistMedia($value)) { else if ($this->isAllowedProtocol($value) && ! $this->isBlacklistedMedia($value)) {
if ($attribute == 'src' && if ($attribute == 'src' &&
isset($attributes['data-src']) && isset($attributes['data-src']) &&
$this->isAllowedProtocol($attributes['data-src']) && $this->isAllowedProtocol($attributes['data-src']) &&
! $this->isBlacklistMedia($attributes['data-src'])) { ! $this->isBlacklistedMedia($attributes['data-src'])) {
$value = $attributes['data-src']; $value = $attributes['data-src'];
} }
@ -232,7 +250,7 @@ class Filter
$used_attributes[] = $attribute; $used_attributes[] = $attribute;
} }
} }
else { else if ($this->validateAttributeValue($attribute, $value)) {
$attr_data .= ' '.$attribute.'="'.$value.'"'; $attr_data .= ' '.$attribute.'="'.$value.'"';
$used_attributes[] = $attribute; $used_attributes[] = $attribute;
@ -241,9 +259,9 @@ class Filter
} }
// Check for required attributes // 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)) { if (! in_array($required_attribute, $used_attributes)) {
@ -258,9 +276,9 @@ class Filter
$this->data .= '<'.$name.$attr_data; $this->data .= '<'.$name.$attr_data;
// Add custom attributes // 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 // 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; $this->strip_content = true;
} }
@ -294,8 +312,6 @@ class Filter
public function getAbsoluteUrl($path, $url) public function getAbsoluteUrl($path, $url)
{ {
//if (! filter_var($url, FILTER_VALIDATE_URL)) return '';
$components = parse_url($url); $components = parse_url($url);
if (! isset($components['scheme'])) $components['scheme'] = 'http'; if (! isset($components['scheme'])) $components['scheme'] = 'http';
@ -325,12 +341,10 @@ class Filter
$length = strlen($url_path); $length = strlen($url_path);
if ($length > 1 && $url_path{$length - 1} !== '/') { if ($length > 1 && $url_path{$length - 1} !== '/') {
$url_path = dirname($url_path).'/'; $url_path = dirname($url_path).'/';
} }
if (substr($path, 0, 2) === './') { if (substr($path, 0, 2) === './') {
$path = substr($path, 2); $path = substr($path, 2);
} }
@ -342,35 +356,33 @@ class Filter
public function isRelativePath($value) public function isRelativePath($value)
{ {
if (strpos($value, 'data:') === 0) return false; if (strpos($value, 'data:') === 0) return false;
return strpos($value, '://') === false && strpos($value, '//') !== 0; return strpos($value, '://') === false && strpos($value, '//') !== 0;
} }
public function isAllowedTag($name) public function isAllowedTag($name)
{ {
return isset($this->allowed_tags[$name]); return isset(self::$whitelist_tags[$name]);
} }
public function isAllowedAttribute($tag, $attribute) 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) public function isResource($attribute)
{ {
return in_array($attribute, $this->protocol_attributes); return in_array($attribute, self::$media_attributes);
} }
public function isAllowedIframeResource($value) public function isAllowedIframeResource($value)
{ {
foreach ($this->iframe_allowed_resources as $url) { foreach (self::$iframe_whitelist as $url) {
if (strpos($value, $url) === 0) { if (strpos($value, $url) === 0) {
return true; return true;
} }
} }
@ -381,10 +393,9 @@ class Filter
public function isAllowedProtocol($value) public function isAllowedProtocol($value)
{ {
foreach ($this->allowed_protocols as $protocol) { foreach (self::$scheme_whitelist as $protocol) {
if (strpos($value, $protocol) === 0) { if (strpos($value, $protocol) === 0) {
return true; 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) { if (strpos($resource, $name) !== false) {
return true; return true;
} }
} }
@ -413,4 +423,14 @@ class Filter
isset($attributes['height']) && isset($attributes['width']) && isset($attributes['height']) && isset($attributes['width']) &&
$attributes['height'] == 1 && $attributes['width'] == 1; $attributes['height'] == 1 && $attributes['width'] == 1;
} }
public function validateAttributeValue($attribute, $value)
{
if (in_array($attribute, self::$integer_attributes)) {
return ctype_digit($value);
}
return true;
}
} }