0
0
mirror of https://github.com/ezyang/htmlpurifier.git synced 2024-12-22 08:21:52 +00:00

[3.1.1] Implement %URI.SecureMunge and %URI.SecureMungeSecretKey, thanks Chris!

- URIFilter->prepare can return false in order to abort loading of the filter
- Implemented post URI filtering. Set member variable $post to true to set a URIFilter as such.

git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/trunk@1772 48356398-32a2-884e-a903-53898d9a118a
This commit is contained in:
Edward Z. Yang 2008-05-26 16:26:47 +00:00
parent 3c4346cb1e
commit 322288e6c0
17 changed files with 215 additions and 28 deletions

6
NEWS
View File

@ -12,6 +12,11 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
3.1.1, unknown release date 3.1.1, unknown release date
! More robust imagecrash protection with height/width CSS with %CSS.MaxImgLength, ! More robust imagecrash protection with height/width CSS with %CSS.MaxImgLength,
and height/width HTML with %HTML.MaxImgLength. and height/width HTML with %HTML.MaxImgLength.
! %URI.SecureMunge for secure URI munging (as opposed to %URI.Munge). Be sure
to set %URI.SecureMungeSecretKey when using this directive. Thanks Chris
for sponsoring this feature.
! Implemented post URI filtering. Set member variable $post to true to set
a URIFilter as such.
- Disable percent height/width attributes for img - Disable percent height/width attributes for img
- AttrValidator operations are now atomic; updates to attributes are not - AttrValidator operations are now atomic; updates to attributes are not
manifest in token until end of operations. This prevents naughty internal manifest in token until end of operations. This prevents naughty internal
@ -52,6 +57,7 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
the Printers for HTML Purifier related tasks. the Printers for HTML Purifier related tasks.
. HTML/CSS Printers must be primed with prepareGenerator($gen_config), otherwise . HTML/CSS Printers must be primed with prepareGenerator($gen_config), otherwise
fatal errors will ensue. fatal errors will ensue.
. URIFilter->prepare can return false in order to abort loading of the filter
3.1.0, released 2008-05-18 3.1.0, released 2008-05-18
# Unnecessary references to objects (vestiges of PHP4) removed from method # Unnecessary references to objects (vestiges of PHP4) removed from method

View File

@ -195,7 +195,7 @@
</directive> </directive>
<directive id="URI.Host"> <directive id="URI.Host">
<file name="HTMLPurifier/URIDefinition.php"> <file name="HTMLPurifier/URIDefinition.php">
<line>57</line> <line>63</line>
</file> </file>
<file name="HTMLPurifier/URIFilter/DisableExternal.php"> <file name="HTMLPurifier/URIFilter/DisableExternal.php">
<line>8</line> <line>8</line>
@ -203,12 +203,12 @@
</directive> </directive>
<directive id="URI.Base"> <directive id="URI.Base">
<file name="HTMLPurifier/URIDefinition.php"> <file name="HTMLPurifier/URIDefinition.php">
<line>58</line> <line>64</line>
</file> </file>
</directive> </directive>
<directive id="URI.DefaultScheme"> <directive id="URI.DefaultScheme">
<file name="HTMLPurifier/URIDefinition.php"> <file name="HTMLPurifier/URIDefinition.php">
<line>65</line> <line>71</line>
</file> </file>
</directive> </directive>
<directive id="URI.AllowedSchemes"> <directive id="URI.AllowedSchemes">
@ -228,7 +228,7 @@
</directive> </directive>
<directive id="URI.Munge"> <directive id="URI.Munge">
<file name="HTMLPurifier/AttrDef/URI.php"> <file name="HTMLPurifier/AttrDef/URI.php">
<line>68</line> <line>72</line>
</file> </file>
</directive> </directive>
<directive id="Core.ColorKeywords"> <directive id="Core.ColorKeywords">
@ -371,4 +371,14 @@
<line>8</line> <line>8</line>
</file> </file>
</directive> </directive>
<directive id="URI.SecureMunge">
<file name="HTMLPurifier/URIFilter/SecureMunge.php">
<line>9</line>
</file>
</directive>
<directive id="URI.SecureMungeSecretKey">
<file name="HTMLPurifier/URIFilter/SecureMunge.php">
<line>10</line>
</file>
</directive>
</usage> </usage>

View File

@ -130,30 +130,26 @@
</p> </p>
<p> <p>
Let's suppose I wanted to write a filter that de-internationalized domain Let's suppose I wanted to write a filter that converted links with a
names by converting them to <a href="http://en.wikipedia.org/wiki/Punycode">Punycode</a>. custom <code>image</code> scheme to its corresponding real path on
Assuming that <code>punycode_encode($input)</code> converts <code>$input</code> to our website:
Punycode and returns <code>false</code> on failure:
</p> </p>
<pre>class HTMLPurifier_URIFilter_ConvertIDNToPunycode extends HTMLPurifier_URIFilter <pre>class HTMLPurifier_URIFilter_TransformImageScheme extends HTMLPurifier_URIFilter
{ {
public $name = 'ConvertIDNToPunycode'; public $name = 'TransformImageScheme';
public function filter(&$uri, $config, $context) { public function filter(&$uri, $config, $context) {
if (is_null($uri->host)) return true; if ($uri->scheme !== 'image') return true;
if ($uri->host == utf8_decode($uri->host)) { $img_name = $uri->path;
// is ASCII, abort // Overwrite the previous URI object
return true; $uri = new HTMLPurifier_URI('http', null, null, null, '/img/' . $img_name . '.png', null, null);
}
$host = punycode_encode($uri->host);
if ($host === false) return false;
$uri->host = $host;
return true; return true;
} }
}</pre> }</pre>
<p> <p>
Notice I did not <code>return $uri;</code>. Notice I did not <code>return $uri;</code>. This filter would turn
<code>image:Foo</code> into <code>/img/Foo.png</code>.
</p> </p>
<h2>Activating your filter</h2> <h2>Activating your filter</h2>
@ -186,6 +182,25 @@ $uri->registerFilter(new HTMLPurifier_URIFilter_<strong>NameOfFilter</strong>())
is set to true. is set to true.
</p> </p>
<h2>Post-filter</h2>
<p>
Remember our TransformImageScheme filter? That filter acted before we had
performed scheme validation; otherwise, the URI would have been filtered
out when it was discovered that there was no image scheme. Well, a post-filter
is run after scheme specific validation, so it's ideal for bulk
post-processing of URIs, including munging. To specify a URI as a post-filter,
set the <code>$post</code> member variable to TRUE.
</p>
<pre>class HTMLPurifier_URIFilter_MyPostFilter extends HTMLPurifier_URIFilter
{
public $name = 'MyPostFilter';
public $post = true;
// ... extra code here
}
</pre>
<h2>Examples</h2> <h2>Examples</h2>
<p> <p>

View File

@ -178,6 +178,7 @@ require 'HTMLPurifier/URIFilter/DisableExternal.php';
require 'HTMLPurifier/URIFilter/DisableExternalResources.php'; require 'HTMLPurifier/URIFilter/DisableExternalResources.php';
require 'HTMLPurifier/URIFilter/HostBlacklist.php'; require 'HTMLPurifier/URIFilter/HostBlacklist.php';
require 'HTMLPurifier/URIFilter/MakeAbsolute.php'; require 'HTMLPurifier/URIFilter/MakeAbsolute.php';
require 'HTMLPurifier/URIFilter/SecureMunge.php';
require 'HTMLPurifier/URIScheme/ftp.php'; require 'HTMLPurifier/URIScheme/ftp.php';
require 'HTMLPurifier/URIScheme/http.php'; require 'HTMLPurifier/URIScheme/http.php';
require 'HTMLPurifier/URIScheme/https.php'; require 'HTMLPurifier/URIScheme/https.php';

View File

@ -172,6 +172,7 @@ require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternal.php';
require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternalResources.php'; require_once $__dir . '/HTMLPurifier/URIFilter/DisableExternalResources.php';
require_once $__dir . '/HTMLPurifier/URIFilter/HostBlacklist.php'; require_once $__dir . '/HTMLPurifier/URIFilter/HostBlacklist.php';
require_once $__dir . '/HTMLPurifier/URIFilter/MakeAbsolute.php'; require_once $__dir . '/HTMLPurifier/URIFilter/MakeAbsolute.php';
require_once $__dir . '/HTMLPurifier/URIFilter/SecureMunge.php';
require_once $__dir . '/HTMLPurifier/URIScheme/ftp.php'; require_once $__dir . '/HTMLPurifier/URIScheme/ftp.php';
require_once $__dir . '/HTMLPurifier/URIScheme/http.php'; require_once $__dir . '/HTMLPurifier/URIScheme/http.php';
require_once $__dir . '/HTMLPurifier/URIScheme/https.php'; require_once $__dir . '/HTMLPurifier/URIScheme/https.php';

View File

@ -50,6 +50,10 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
$result = $scheme_obj->validate($uri, $config, $context); $result = $scheme_obj->validate($uri, $config, $context);
if (!$result) break; if (!$result) break;
// Post chained filtering
$result = $uri_def->postFilter($uri, $config, $context);
if (!$result) break;
// survived gauntlet // survived gauntlet
$ok = true; $ok = true;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
URI.SecureMunge
TYPE: string/null
VERSION: 3.1.1
DEFAULT: NULL
--DESCRIPTION--
<p>
Like %URI.Munge, this directive munges browsable external resources
into another URI redirection service. %URI.SecureMunge accepts a URI
with a %s located where the original URI should be substituted in,
and %t located where the secure checksum should be provided.
However, this directive affords
an additional level of protection by generating a secure checksum from
the URI as well as a secret key provided by %URI.SecureMungeSecretKey.
Any redirector script can check this key by using:
</p>
<pre>$checksum === sha1($secret_key . ':' . $url)</pre>
<p>
If the output is TRUE, the redirector script should accept the URI.
</p>
<p>
Please note that it would still be possible for an attacker to procure
secure hashes en-mass by abusing your website's Preview feature or the
like, but this service affords an additional level of protection
that should be combined with website blacklisting.
</p>
<p>
<strong>This is a post-filter.</strong> This filter may conflict with other
post-filters that deal with external links.
</p>

View File

@ -0,0 +1,11 @@
URI.SecureMungeSecretKey
TYPE: string/null
VERSION: 3.1.1
DEFAULT: NULL
--DESCRIPTION--
<p>
This is the secret key used in conjunction with %URI.SecureMunge. Your
redirector script needs to know about this key, and no one else should
know about this key. Please see the above
directive for more details.
</p>

View File

@ -5,6 +5,7 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
public $type = 'URI'; public $type = 'URI';
protected $filters = array(); protected $filters = array();
protected $postFilters = array();
protected $registeredFilters = array(); protected $registeredFilters = array();
/** /**
@ -34,8 +35,13 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
} }
public function addFilter($filter, $config) { public function addFilter($filter, $config) {
$filter->prepare($config); $r = $filter->prepare($config);
$this->filters[$filter->name] = $filter; if ($r === false) return; // null is ok, for backwards compat
if ($filter->post) {
$this->postFilters[$filter->name] = $filter;
} else {
$this->filters[$filter->name] = $filter;
}
} }
protected function doSetup($config) { protected function doSetup($config) {
@ -66,8 +72,16 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
} }
public function filter(&$uri, $config, $context) { public function filter(&$uri, $config, $context) {
foreach ($this->filters as $name => $x) { foreach ($this->filters as $name => $f) {
$result = $this->filters[$name]->filter($uri, $config, $context); $result = $f->filter($uri, $config, $context);
if (!$result) return false;
}
return true;
}
public function postFilter(&$uri, $config, $context) {
foreach ($this->postFilters as $name => $f) {
$result = $f->filter($uri, $config, $context);
if (!$result) return false; if (!$result) return false;
} }
return true; return true;

View File

@ -19,10 +19,15 @@ abstract class HTMLPurifier_URIFilter
*/ */
public $name; public $name;
/**
* True if this filter should be run after scheme validation.
*/
public $post = false;
/** /**
* Performs initialization for the filter * Performs initialization for the filter
*/ */
public function prepare($config) {} public function prepare($config) {return true;}
/** /**
* Filter a URI object * Filter a URI object

View File

@ -6,6 +6,7 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter
protected $blacklist = array(); protected $blacklist = array();
public function prepare($config) { public function prepare($config) {
$this->blacklist = $config->get('URI', 'HostBlacklist'); $this->blacklist = $config->get('URI', 'HostBlacklist');
return true;
} }
public function filter(&$uri, $config, $context) { public function filter(&$uri, $config, $context) {
foreach($this->blacklist as $blacklisted_host_fragment) { foreach($this->blacklist as $blacklisted_host_fragment) {

View File

@ -11,14 +11,15 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter
$def = $config->getDefinition('URI'); $def = $config->getDefinition('URI');
$this->base = $def->base; $this->base = $def->base;
if (is_null($this->base)) { if (is_null($this->base)) {
trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_ERROR); trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING);
return; return false;
} }
$this->base->fragment = null; // fragment is invalid for base URI $this->base->fragment = null; // fragment is invalid for base URI
$stack = explode('/', $this->base->path); $stack = explode('/', $this->base->path);
array_pop($stack); // discard last segment array_pop($stack); // discard last segment
$stack = $this->_collapseStack($stack); // do pre-parsing $stack = $this->_collapseStack($stack); // do pre-parsing
$this->basePathStack = $stack; $this->basePathStack = $stack;
return true;
} }
public function filter(&$uri, $config, $context) { public function filter(&$uri, $config, $context) {
if (is_null($this->base)) return true; // abort early if (is_null($this->base)) return true; // abort early

View File

@ -0,0 +1,32 @@
<?php
class HTMLPurifier_URIFilter_SecureMunge extends HTMLPurifier_URIFilter
{
public $name = 'SecureMunge';
public $post = true;
private $target, $secretKey, $parser;
public function prepare($config) {
$this->target = $config->get('URI', 'SecureMunge');
$this->secretKey = $config->get('URI', 'SecureMungeSecretKey');
$this->parser = new HTMLPurifier_URIParser();
if (!$this->secretKey) {
trigger_error('URI.SecureMunge is being ignored due to lack of value for URI.SecureMungeSecretKey', E_USER_WARNING);
return false;
}
return true;
}
public function filter(&$uri, $config, $context) {
if (!$this->target || !$this->secretKey) return true;
$scheme_obj = $uri->getSchemeObj($config, $context);
if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it
if (is_null($uri->host) || empty($scheme_obj->browsable)) {
return true;
}
$string = $uri->toString();
$checksum = sha1($this->secretKey . ':' . $string);
$new_uri = str_replace('%s', rawurlencode($string), $this->target);
$new_uri = str_replace('%t', $checksum, $new_uri);
$uri = $this->parser->parse($new_uri); // overwrite
return true;
}
}

View File

@ -83,6 +83,8 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness
$uri_def = new HTMLPurifier_URIDefinitionMock(); $uri_def = new HTMLPurifier_URIDefinitionMock();
$uri_def->expectOnce('filter', array($uri, '*', '*')); $uri_def->expectOnce('filter', array($uri, '*', '*'));
$uri_def->setReturnValue('filter', true, array($uri, '*', '*')); $uri_def->setReturnValue('filter', true, array($uri, '*', '*'));
$uri_def->expectOnce('postFilter', array($uri, '*', '*'));
$uri_def->setReturnValue('postFilter', true, array($uri, '*', '*'));
$uri_def->setup = true; $uri_def->setup = true;
// Since definitions are no longer passed by reference, we need // Since definitions are no longer passed by reference, we need

View File

@ -3,14 +3,16 @@
class HTMLPurifier_URIDefinitionTest extends HTMLPurifier_URIHarness class HTMLPurifier_URIDefinitionTest extends HTMLPurifier_URIHarness
{ {
protected function createFilterMock($expect = true, $result = true) { protected function createFilterMock($expect = true, $result = true, $post = false, $setup = true) {
static $i = 0; static $i = 0;
generate_mock_once('HTMLPurifier_URIFilter'); generate_mock_once('HTMLPurifier_URIFilter');
$mock = new HTMLPurifier_URIFilterMock(); $mock = new HTMLPurifier_URIFilterMock();
if ($expect) $mock->expectOnce('filter'); if ($expect) $mock->expectOnce('filter');
else $mock->expectNever('filter'); else $mock->expectNever('filter');
$mock->setReturnValue('filter', $result); $mock->setReturnValue('filter', $result);
$mock->setReturnValue('prepare', $setup);
$mock->name = $i++; $mock->name = $i++;
$mock->post = $post;
return $mock; return $mock;
} }

View File

@ -0,0 +1,49 @@
<?php
class HTMLPurifier_URIFilter_SecureMungeTest extends HTMLPurifier_URIFilterHarness
{
function setUp() {
parent::setUp();
$this->filter = new HTMLPurifier_URIFilter_SecureMunge();
$this->setSecureMunge();
$this->setSecretKey();
}
function setSecureMunge($uri = '/redirect.php?url=%s&checksum=%t') {
$this->config->set('URI', 'SecureMunge', $uri);
}
function setSecretKey($key = 'secret') {
$this->config->set('URI', 'SecureMungeSecretKey', $key);
}
function testPreserve() {
$this->assertFiltering('/local');
}
function testStandardMunge() {
$this->assertFiltering('http://google.com', '/redirect.php?url=http%3A%2F%2Fgoogle.com&checksum=0072e2f817fd2844825def74e54443debecf0892');
}
function testIgnoreUnknownSchemes() {
// This should be integration tested as well to be false
$this->assertFiltering('javascript:', true);
}
function testIgnoreUnbrowsableSchemes() {
$this->assertFiltering('news:', true);
}
function testMungeToDirectory() {
$this->setSecureMunge('/links/%s/%t');
$this->assertFiltering('http://google.com', '/links/http%3A%2F%2Fgoogle.com/0072e2f817fd2844825def74e54443debecf0892');
}
function testErrorNoSecretKey() {
$this->setSecretKey(null);
$this->expectError('URI.SecureMunge is being ignored due to lack of value for URI.SecureMungeSecretKey');
$this->assertFiltering('http://google.com');
}
}