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

[3.1.1] General munge improvements

- Add CurrentCSSProperty context variable
- Move Munge to its own class, derived off of SecureMunge.
- Rename %URI.SecureMunge to %URI.Munge
- Rename %URI.SecureMungeSecretKey to %URI.MungeSecretKey
- Add extra substitutions for munge

git-svn-id: http://htmlpurifier.org/svnroot/htmlpurifier/trunk@1803 48356398-32a2-884e-a903-53898d9a118a
This commit is contained in:
Edward Z. Yang 2008-06-18 03:29:27 +00:00
parent 7189ec2790
commit 463aa3a0fa
18 changed files with 299 additions and 171 deletions

13
NEWS
View File

@ -12,14 +12,19 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
3.1.1, unknown release date
! More robust imagecrash protection with height/width CSS with %CSS.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.
! %URI.SecureMunge for secure URI munging (as opposed to %URI.Munge). Thanks Chris
for sponsoring this feature. Check out the corresponding documentation
for details. (Att Nightly testers: The API for this feature changed before
the general release. Namely, rename your directives %URI.SecureMungeSecretKey =>
%URI.MungeSecretKey and and %URI.SecureMunge => %URI.Munge)
! Implemented post URI filtering. Set member variable $post to true to set
a URIFilter as such.
! Allow modules to define injectors via $info_injector. Injectors are
automatically disabled if injector's needed elements are not found.
! Support for "safe" objects added, use %HTML.SafeObject and %HTML.SafeEmbed.
! Added substitutions for %e, %n, %a and %p in %URI.Munge (in order,
embedded, tag name, attribute name, CSS property name). See %URI.Munge
for more details.
- Disable percent height/width attributes for img
- AttrValidator operations are now atomic; updates to attributes are not
manifest in token until end of operations. This prevents naughty internal
@ -67,6 +72,8 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier
. URIFilter->prepare can return false in order to abort loading of the filter
. Factory for AttrDef_URI implemented, URI#embedded to indicate URI that embeds
an external resource.
. %URI.Munge functionality factored out into a post-filter class.
. Added CurrentCSSProperty context variable during CSS validation
3.1.0, released 2008-05-18
# Unnecessary references to objects (vestiges of PHP4) removed from method

View File

@ -236,11 +236,6 @@
<line>28</line>
</file>
</directive>
<directive id="URI.Munge">
<file name="HTMLPurifier/AttrDef/URI.php">
<line>77</line>
</file>
</directive>
<directive id="Core.ColorKeywords">
<file name="HTMLPurifier/AttrDef/CSS/Color.php">
<line>12</line>
@ -387,14 +382,14 @@
<line>8</line>
</file>
</directive>
<directive id="URI.SecureMunge">
<file name="HTMLPurifier/URIFilter/SecureMunge.php">
<line>9</line>
<directive id="URI.MungeResources">
<file name="HTMLPurifier/URIFilter/Munge.php">
<line>14</line>
</file>
</directive>
<directive id="URI.SecureMungeSecretKey">
<file name="HTMLPurifier/URIFilter/SecureMunge.php">
<line>10</line>
<directive id="URI.MungeSecretKey">
<file name="HTMLPurifier/URIFilter/Munge.php">
<line>15</line>
</file>
</directive>
</usage>

View File

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

View File

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

View File

@ -29,6 +29,12 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
$declarations = explode(';', $css);
$propvalues = array();
/**
* Name of the current CSS property being validated.
*/
$property = false;
$context->register('CurrentCSSProperty', $property);
foreach ($declarations as $declaration) {
if (!$declaration) continue;
if (!strpos($declaration, ':')) continue;
@ -61,6 +67,8 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef
$propvalues[$property] = $result;
}
$context->destroy('CurrentCSSProperty');
// procedure does not write the new CSS simultaneously, so it's
// slightly inefficient, but it's the only way of getting rid of
// duplicates. Perhaps config to optimize it, but not now.

View File

@ -68,18 +68,7 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef
if (!$ok) return false;
// back to string
$result = $uri->toString();
// munge entire URI if necessary
if (
!is_null($uri->host) && // indicator for authority
!empty($scheme_obj->browsable) &&
!is_null($munge = $config->get('URI', 'Munge'))
) {
$result = str_replace('%s', rawurlencode($result), $munge);
}
return $result;
return $uri->toString();
}

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ DEFAULT: NULL
<p>
Munges all browsable (usually http, https and ftp)
absolute URI's into another URI, usually a URI redirection service.
absolute URIs into another URI, usually a URI redirection service.
This directive accepts a URI, formatted with a <code>%s</code> where
the url-encoded original URI should be inserted (sample:
<code>http://www.google.com/url?q=%s</code>).
@ -19,13 +19,58 @@ DEFAULT: NULL
Prevent PageRank leaks, while being fairly transparent
to users (you may also want to add some client side JavaScript to
override the text in the statusbar). <strong>Notice</strong>:
Many security experts believe that this form of protection does
not deter spam-bots.
Many security experts believe that this form of protection does not deter spam-bots.
</li>
<li>
Redirect users to a splash page telling them they are leaving your
website. While this is poor usability practice, it is often
mandated
website. While this is poor usability practice, it is often mandated
in corporate environments.
</li>
</ul>
<p>
You may want to also use %URI.MungeSecretKey along with this directive
in order to enforce what URIs your redirector script allows. Open
redirector scripts can be a security risk and negatively affect the
reputation of your domain name.
</p>
<p>
Starting with HTML Purifier 3.1.1, there is also these substitutions:
</p>
<table>
<thead>
<tr>
<th>Key</th>
<th>Description</th>
<th>Example <code>&lt;a href=""&gt;</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>%r</td>
<td>1 - The URI embeds a resource<br />(blank) - The URI is merely a link</td>
<td></td>
</tr>
<tr>
<td>%n</td>
<td>The name of the tag this URI came from</td>
<td>a</td>
</tr>
<tr>
<td>%m</td>
<td>The name of the attribute this URI came from</td>
<td>href</td>
</tr>
<tr>
<td>%p</td>
<td>The name of the CSS property this URI came from, or blank if irrelevant</td>
<td></td>
</tr>
</tbody>
</table>
<p>
Admittedly, these letters are somewhat arbitrary; the only stipulation
was that they couldn't be a through f. r is for resource (I would have preferred
e, but you take what you can get), n is for name, m
was picked because it came after n (and I couldn't use a), p is for
property.
</p>

View File

@ -0,0 +1,12 @@
URI.MungeResources
TYPE: bool
VERSION: 3.1.1
DEFAULT: false
--DESCRIPTION--
<p>
If true, any URI munging directives like %URI.Munge or %URI.SecureMunge
will also apply to embedded resources, such as <code>&lt;img src=""&gt;</code>.
Be careful enabling this directive if you have a redirector script
that does not use the <code>Location</code> HTTP header; all of your images
and other embedded resources will break.
</ul>

View File

@ -0,0 +1,29 @@
URI.MungeSecretKey
TYPE: string/null
VERSION: 3.1.1
DEFAULT: NULL
--DESCRIPTION--
<p>
This directive enables secure checksum generation along with %URI.Munge.
It should be set to a secure key that is not shared with anyone else.
The checksum can be placed in the URI using %t. Use of this checksum
affords an additional level of protection by allowing a redirector
to check if a URI has passed through HTML Purifier with this line:
</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>
Remember this has no effect if %URI.Munge is not on.
</p>

View File

@ -1,33 +0,0 @@
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

@ -1,11 +0,0 @@
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

@ -28,7 +28,7 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition
$this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources());
$this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist());
$this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute());
$this->registerFilter(new HTMLPurifier_URIFilter_SecureMunge());
$this->registerFilter(new HTMLPurifier_URIFilter_Munge());
}
public function registerFilter($filter) {

View File

@ -0,0 +1,48 @@
<?php
class HTMLPurifier_URIFilter_Munge extends HTMLPurifier_URIFilter
{
public $name = 'Munge';
public $post = true;
private $target, $parser, $doEmbed, $secretKey;
protected $replace = array();
public function prepare($config) {
$this->target = $config->get('URI', $this->name);
$this->parser = new HTMLPurifier_URIParser();
$this->doEmbed = $config->get('URI', 'MungeResources');
$this->secretKey = $config->get('URI', 'MungeSecretKey');
return true;
}
public function filter(&$uri, $config, $context) {
if ($context->get('EmbeddedURI', true) && !$this->doEmbed) 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;
}
$this->makeReplace($uri, $config, $context);
$this->replace = array_map('rawurlencode', $this->replace);
$new_uri = strtr($this->target, $this->replace);
$uri = $this->parser->parse($new_uri); // overwrite
return true;
}
protected function makeReplace($uri, $config, $context) {
$string = $uri->toString();
// always available
$this->replace['%s'] = $string;
$this->replace['%r'] = $context->get('EmbeddedURI', true);
$token = $context->get('CurrentToken', true);
$this->replace['%n'] = $token ? $token->name : null;
$this->replace['%m'] = $context->get('CurrentAttr', true);
$this->replace['%p'] = $context->get('CurrentCSSProperty', true);
// not always available
if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string);
}
}

View File

@ -1,33 +0,0 @@
<?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;
if ($context->get('EmbeddedURI', true)) return true; // abort for embedded URIs
$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

@ -0,0 +1,115 @@
<?php
class HTMLPurifier_URIFilter_MungeTest extends HTMLPurifier_URIFilterHarness
{
function setUp() {
parent::setUp();
$this->filter = new HTMLPurifier_URIFilter_Munge();
}
protected function setMunge($uri = 'http://www.google.com/url?q=%s') {
$this->config->set('URI', 'Munge', $uri);
}
protected function setSecureMunge($key = 'secret') {
$this->setMunge('/redirect.php?url=%s&checksum=%t');
$this->config->set('URI', 'MungeSecretKey', $key);
}
function testMunge() {
$this->setMunge();
$this->assertFiltering(
'http://www.example.com/',
'http://www.google.com/url?q=http%3A%2F%2Fwww.example.com%2F'
);
}
function testMungeReplaceTagName() {
$this->setMunge('/r?tagname=%n&url=%s');
$token = new HTMLPurifier_Token_Start('a');
$this->context->register('CurrentToken', $token);
$this->assertFiltering('http://google.com', '/r?tagname=a&url=http%3A%2F%2Fgoogle.com');
}
function testMungeReplaceAttribute() {
$this->setMunge('/r?attr=%m&url=%s');
$attr = 'href';
$this->context->register('CurrentAttr', $attr);
$this->assertFiltering('http://google.com', '/r?attr=href&url=http%3A%2F%2Fgoogle.com');
}
function testMungeReplaceResource() {
$this->setMunge('/r?embeds=%r&url=%s');
$embeds = false;
$this->context->register('EmbeddedURI', $embeds);
$this->assertFiltering('http://google.com', '/r?embeds=&url=http%3A%2F%2Fgoogle.com');
}
function testMungeReplaceCSSProperty() {
$this->setMunge('/r?property=%p&url=%s');
$property = 'background';
$this->context->register('CurrentCSSProperty', $property);
$this->assertFiltering('http://google.com', '/r?property=background&url=http%3A%2F%2Fgoogle.com');
}
function testIgnoreEmbedded() {
$this->setMunge();
$embeds = true;
$this->context->register('EmbeddedURI', $embeds);
$this->assertFiltering('http://example.com');
}
function testProcessEmbedded() {
$this->setMunge();
$this->config->set('URI', 'MungeResources', true);
$embeds = true;
$this->context->register('EmbeddedURI', $embeds);
$this->assertFiltering('http://www.example.com/', 'http://www.google.com/url?q=http%3A%2F%2Fwww.example.com%2F');
}
function testPreserveRelative() {
$this->setMunge();
$this->assertFiltering('index.html');
}
function testMungeIgnoreUnknownSchemes() {
$this->setMunge();
$this->assertFiltering('javascript:foobar();', true);
}
function testSecureMungePreserve() {
$this->setSecureMunge();
$this->assertFiltering('/local');
}
function testSecureMungePreserveEmbedded() {
$this->setSecureMunge();
$embedded = true;
$this->context->register('EmbeddedURI', $embedded);
$this->assertFiltering('http://google.com');
}
function testSecureMungeStandard() {
$this->setSecureMunge();
$this->assertFiltering('http://google.com', '/redirect.php?url=http%3A%2F%2Fgoogle.com&checksum=0072e2f817fd2844825def74e54443debecf0892');
}
function testSecureMungeIgnoreUnknownSchemes() {
// This should be integration tested as well to be false
$this->setSecureMunge();
$this->assertFiltering('javascript:', true);
}
function testSecureMungeIgnoreUnbrowsableSchemes() {
$this->setSecureMunge();
$this->assertFiltering('news:', true);
}
function testSecureMungeToDirectory() {
$this->setSecureMunge();
$this->setMunge('/links/%s/%t');
$this->assertFiltering('http://google.com', '/links/http%3A%2F%2Fgoogle.com/0072e2f817fd2844825def74e54443debecf0892');
}
}

View File

@ -1,55 +0,0 @@
<?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 testPreserveEmbedded() {
$embedded = true;
$this->context->register('EmbeddedURI', $embedded);
$this->assertFiltering('http://google.com');
}
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');
}
}

View File

@ -186,8 +186,8 @@ alert("<This is compatible with XHTML>");
}
function test_secureMunge() {
$this->config->set('URI', 'SecureMunge', '/redirect.php?url=%s&check=%t');
$this->config->set('URI', 'SecureMungeSecretKey', 'foo');
$this->config->set('URI', 'Munge', '/redirect.php?url=%s&check=%t');
$this->config->set('URI', 'MungeSecretKey', 'foo');
$this->assertPurification(
'<a href="http://localhost">foo</a><img src="http://localhost" alt="local" />',
'<a href="/redirect.php?url=http%3A%2F%2Flocalhost&amp;check=8e8223ae8fac24561104180ea549c21fbd111be7">foo</a><img src="http://localhost" alt="local" />'
@ -206,13 +206,25 @@ alert("<This is compatible with XHTML>");
function test_safeObjectAndEmbedWithSecureMunge() {
$this->config->set('HTML', 'SafeObject', true);
$this->config->set('HTML', 'SafeEmbed', true);
$this->config->set('URI', 'SecureMunge', '/redirect.php?url=%s&check=%t');
$this->config->set('URI', 'SecureMungeSecretKey', 'foo');
$this->config->set('URI', 'Munge', '/redirect.php?url=%s&check=%t');
$this->config->set('URI', 'MungeSecretKey', 'foo');
$this->assertPurification(
'<object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/Oq3FV_zdyy0&hl=en"></param><embed src="http://www.youtube.com/v/Oq3FV_zdyy0&hl=en" type="application/x-shockwave-flash" width="425" height="344"></embed></object>',
'<object width="425" height="344" data="http://www.youtube.com/v/Oq3FV_zdyy0&amp;hl=en" type="application/x-shockwave-flash"><param name="allowScriptAccess" value="never" /><param name="allowNetworking" value="internal" /><param name="movie" value="http://www.youtube.com/v/Oq3FV_zdyy0&amp;hl=en" /><embed src="http://www.youtube.com/v/Oq3FV_zdyy0&amp;hl=en" type="application/x-shockwave-flash" width="425" height="344" allowscriptaccess="never" allownetworking="internal" /></object>'
);
}
function test_mungeWithExtraParams() {
$this->config->set('URI', 'Munge', '/redirect?s=%s&t=%t&r=%r&n=%n&m=%m&p=%p');
$this->config->set('URI', 'MungeSecretKey', 'foo');
$this->config->set('URI', 'MungeResources', true);
$this->assertPurification(
'<a href="http://example.com">Link</a><img src="http://example.com" style="background-image:url(http://example.com);" alt="example.com" />',
'<a href="/redirect?s=http%3A%2F%2Fexample.com&amp;t=c15354f3953dfec262c55b1403067e0d045a3059&amp;r=&amp;n=a&amp;m=href&amp;p=">Link</a>'.
'<img src="/redirect?s=http%3A%2F%2Fexample.com&amp;t=c15354f3953dfec262c55b1403067e0d045a3059&amp;r=1&amp;n=img&amp;m=src&amp;p=" '.
'style="background-image:url(/redirect?s=http%3A%2F%2Fexample.com&amp;t=c15354f3953dfec262c55b1403067e0d045a3059&amp;r=1&amp;n=img&amp;m=style&amp;p=background-image);" alt="example.com" />'
);
}
}