From 322288e6c07c911dcb76e8ec0aa521b200edd23f Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Mon, 26 May 2008 16:26:47 +0000 Subject: [PATCH] [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 --- NEWS | 6 +++ configdoc/usage.xml | 18 +++++-- docs/enduser-uri-filter.html | 45 +++++++++++------ library/HTMLPurifier.includes.php | 1 + library/HTMLPurifier.safe-includes.php | 1 + library/HTMLPurifier/AttrDef/URI.php | 4 ++ library/HTMLPurifier/ConfigSchema/schema.ser | 2 +- .../ConfigSchema/schema/URI.SecureMunge.txt | 33 +++++++++++++ .../schema/URI.SecureMungeSecretKey.txt | 11 +++++ library/HTMLPurifier/URIDefinition.php | 22 +++++++-- library/HTMLPurifier/URIFilter.php | 7 ++- .../HTMLPurifier/URIFilter/HostBlacklist.php | 1 + .../HTMLPurifier/URIFilter/MakeAbsolute.php | 5 +- .../HTMLPurifier/URIFilter/SecureMunge.php | 32 ++++++++++++ tests/HTMLPurifier/AttrDef/URITest.php | 2 + tests/HTMLPurifier/URIDefinitionTest.php | 4 +- .../URIFilter/SecureMungeTest.php | 49 +++++++++++++++++++ 17 files changed, 215 insertions(+), 28 deletions(-) create mode 100644 library/HTMLPurifier/ConfigSchema/schema/URI.SecureMunge.txt create mode 100644 library/HTMLPurifier/ConfigSchema/schema/URI.SecureMungeSecretKey.txt create mode 100644 library/HTMLPurifier/URIFilter/SecureMunge.php create mode 100644 tests/HTMLPurifier/URIFilter/SecureMungeTest.php diff --git a/NEWS b/NEWS index d0e5b1d7..a4b4ebe9 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,11 @@ 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. +! Implemented post URI filtering. Set member variable $post to true to set + a URIFilter as such. - 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 @@ -52,6 +57,7 @@ NEWS ( CHANGELOG and HISTORY ) HTMLPurifier the Printers for HTML Purifier related tasks. . HTML/CSS Printers must be primed with prepareGenerator($gen_config), otherwise fatal errors will ensue. +. URIFilter->prepare can return false in order to abort loading of the filter 3.1.0, released 2008-05-18 # Unnecessary references to objects (vestiges of PHP4) removed from method diff --git a/configdoc/usage.xml b/configdoc/usage.xml index 3b68cd7a..c5f149ac 100644 --- a/configdoc/usage.xml +++ b/configdoc/usage.xml @@ -195,7 +195,7 @@ - 57 + 63 8 @@ -203,12 +203,12 @@ - 58 + 64 - 65 + 71 @@ -228,7 +228,7 @@ - 68 + 72 @@ -371,4 +371,14 @@ 8 + + + 9 + + + + + 10 + + diff --git a/docs/enduser-uri-filter.html b/docs/enduser-uri-filter.html index b0a377a4..23d1d3dc 100644 --- a/docs/enduser-uri-filter.html +++ b/docs/enduser-uri-filter.html @@ -130,30 +130,26 @@

- Let's suppose I wanted to write a filter that de-internationalized domain - names by converting them to Punycode. - Assuming that punycode_encode($input) converts $input to - Punycode and returns false on failure: + Let's suppose I wanted to write a filter that converted links with a + custom image scheme to its corresponding real path on + our website:

-
class HTMLPurifier_URIFilter_ConvertIDNToPunycode extends HTMLPurifier_URIFilter
+
class HTMLPurifier_URIFilter_TransformImageScheme extends HTMLPurifier_URIFilter
 {
-    public $name = 'ConvertIDNToPunycode';
+    public $name = 'TransformImageScheme';
     public function filter(&$uri, $config, $context) {
-        if (is_null($uri->host)) return true;
-        if ($uri->host == utf8_decode($uri->host)) {
-            // is ASCII, abort
-            return true;
-        }
-        $host = punycode_encode($uri->host);
-        if ($host === false) return false;
-        $uri->host = $host;
+        if ($uri->scheme !== 'image') return true;
+        $img_name = $uri->path;
+        // Overwrite the previous URI object
+        $uri = new HTMLPurifier_URI('http', null, null, null, '/img/' . $img_name . '.png', null, null);
         return true;
     }
 }

- Notice I did not return $uri;. + Notice I did not return $uri;. This filter would turn + image:Foo into /img/Foo.png.

Activating your filter

@@ -186,6 +182,25 @@ $uri->registerFilter(new HTMLPurifier_URIFilter_NameOfFilter()) is set to true.

+

Post-filter

+ +

+ 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 $post member variable to TRUE. +

+ +
class HTMLPurifier_URIFilter_MyPostFilter extends HTMLPurifier_URIFilter
+{
+    public $name = 'MyPostFilter';
+    public $post = true;
+    // ... extra code here
+}
+
+

Examples

diff --git a/library/HTMLPurifier.includes.php b/library/HTMLPurifier.includes.php index b0d0b2ec..7fc82f2d 100644 --- a/library/HTMLPurifier.includes.php +++ b/library/HTMLPurifier.includes.php @@ -178,6 +178,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/URIScheme/ftp.php'; require 'HTMLPurifier/URIScheme/http.php'; require 'HTMLPurifier/URIScheme/https.php'; diff --git a/library/HTMLPurifier.safe-includes.php b/library/HTMLPurifier.safe-includes.php index f82675a6..3bda31d6 100644 --- a/library/HTMLPurifier.safe-includes.php +++ b/library/HTMLPurifier.safe-includes.php @@ -172,6 +172,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/URIScheme/ftp.php'; require_once $__dir . '/HTMLPurifier/URIScheme/http.php'; require_once $__dir . '/HTMLPurifier/URIScheme/https.php'; diff --git a/library/HTMLPurifier/AttrDef/URI.php b/library/HTMLPurifier/AttrDef/URI.php index 9c632f1a..99bdbcc3 100644 --- a/library/HTMLPurifier/AttrDef/URI.php +++ b/library/HTMLPurifier/AttrDef/URI.php @@ -50,6 +50,10 @@ class HTMLPurifier_AttrDef_URI extends HTMLPurifier_AttrDef $result = $scheme_obj->validate($uri, $config, $context); if (!$result) break; + // Post chained filtering + $result = $uri_def->postFilter($uri, $config, $context); + if (!$result) break; + // survived gauntlet $ok = true; diff --git a/library/HTMLPurifier/ConfigSchema/schema.ser b/library/HTMLPurifier/ConfigSchema/schema.ser index 2b76b129..deb73b34 100644 --- a/library/HTMLPurifier/ConfigSchema/schema.ser +++ b/library/HTMLPurifier/ConfigSchema/schema.ser @@ -1 +1 @@ -O:25:"HTMLPurifier_ConfigSchema":2:{s:8:"defaults";a:12:{s:4:"Attr";a:11:{s:19:"AllowedFrameTargets";a:0:{}s:10:"AllowedRel";a:0:{}s:10:"AllowedRev";a:0:{}s:19:"DefaultInvalidImage";s:0:"";s:22:"DefaultInvalidImageAlt";s:13:"Invalid image";s:14:"DefaultTextDir";s:3:"ltr";s:8:"EnableID";b:0;s:11:"IDBlacklist";a:0:{}s:17:"IDBlacklistRegexp";N;s:8:"IDPrefix";s:0:"";s:13:"IDPrefixLocal";s:0:"";}s:10:"AutoFormat";a:4:{s:13:"AutoParagraph";b:0;s:6:"Custom";a:0:{}s:7:"Linkify";b:0;s:15:"PurifierLinkify";b:0;}s:15:"AutoFormatParam";a:1:{s:21:"PurifierLinkifyDocURL";s:3:"#%s";}s:3:"CSS";a:6:{s:14:"AllowImportant";b:0;s:11:"AllowTricky";b:0;s:17:"AllowedProperties";N;s:13:"DefinitionRev";i:1;s:12:"MaxImgLength";s:6:"1200px";s:11:"Proprietary";b:0;}s:5:"Cache";a:2:{s:14:"DefinitionImpl";s:10:"Serializer";s:14:"SerializerPath";N;}s:4:"Core";a:15:{s:17:"AggressivelyFixLt";b:0;s:13:"CollectErrors";b:0;s:13:"ColorKeywords";a:17:{s:6:"maroon";s:7:"#800000";s:3:"red";s:7:"#FF0000";s:6:"orange";s:7:"#FFA500";s:6:"yellow";s:7:"#FFFF00";s:5:"olive";s:7:"#808000";s:6:"purple";s:7:"#800080";s:7:"fuchsia";s:7:"#FF00FF";s:5:"white";s:7:"#FFFFFF";s:4:"lime";s:7:"#00FF00";s:5:"green";s:7:"#008000";s:4:"navy";s:7:"#000080";s:4:"blue";s:7:"#0000FF";s:4:"aqua";s:7:"#00FFFF";s:4:"teal";s:7:"#008080";s:5:"black";s:7:"#000000";s:6:"silver";s:7:"#C0C0C0";s:4:"gray";s:7:"#808080";}s:25:"ConvertDocumentToFragment";b:1;s:31:"DirectLexLineNumberSyncInterval";i:0;s:8:"Encoding";s:5:"utf-8";s:21:"EscapeInvalidChildren";b:0;s:17:"EscapeInvalidTags";b:0;s:24:"EscapeNonASCIICharacters";b:0;s:14:"HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:8:"Language";s:2:"en";s:9:"LexerImpl";N;s:19:"MaintainLineNumbers";N;s:16:"RemoveInvalidImg";b:1;s:20:"RemoveScriptContents";N;}s:6:"Filter";a:3:{s:6:"Custom";a:0:{}s:18:"ExtractStyleBlocks";b:0;s:7:"YouTube";b:0;}s:11:"FilterParam";a:3:{s:26:"ExtractStyleBlocksEscaping";b:1;s:23:"ExtractStyleBlocksScope";N;s:26:"ExtractStyleBlocksTidyImpl";N;}s:4:"HTML";a:21:{s:7:"Allowed";N;s:17:"AllowedAttributes";N;s:15:"AllowedElements";N;s:14:"AllowedModules";N;s:12:"BlockWrapper";s:1:"p";s:11:"CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:13:"CustomDoctype";N;s:12:"DefinitionID";N;s:13:"DefinitionRev";i:1;s:7:"Doctype";N;s:19:"ForbiddenAttributes";a:0:{}s:17:"ForbiddenElements";a:0:{}s:12:"MaxImgLength";i:1200;s:6:"Parent";s:3:"div";s:11:"Proprietary";b:0;s:6:"Strict";b:0;s:7:"TidyAdd";a:0:{}s:9:"TidyLevel";s:6:"medium";s:10:"TidyRemove";a:0:{}s:7:"Trusted";b:0;s:5:"XHTML";b:1;}s:6:"Output";a:3:{s:21:"CommentScriptContents";b:1;s:7:"Newline";N;s:10:"TidyFormat";b:0;}s:4:"Test";a:1:{s:12:"ForceNoIconv";b:0;}s:3:"URI";a:14:{s:14:"AllowedSchemes";a:6:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;}s:4:"Base";N;s:13:"DefaultScheme";s:4:"http";s:12:"DefinitionID";N;s:13:"DefinitionRev";i:1;s:7:"Disable";b:0;s:15:"DisableExternal";b:0;s:24:"DisableExternalResources";b:0;s:16:"DisableResources";b:0;s:4:"Host";N;s:13:"HostBlacklist";a:0:{}s:12:"MakeAbsolute";b:0;s:5:"Munge";N;s:22:"OverrideAllowedSchemes";b:1;}}s:4:"info";a:12:{s:4:"Attr";a:12:{s:19:"AllowedFrameTargets";i:8;s:10:"AllowedRel";i:8;s:10:"AllowedRev";i:8;s:19:"DefaultInvalidImage";i:1;s:22:"DefaultInvalidImageAlt";i:1;s:14:"DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:8:"EnableID";i:7;s:11:"IDBlacklist";i:9;s:17:"IDBlacklistRegexp";i:-1;s:8:"IDPrefix";i:1;s:13:"IDPrefixLocal";i:1;s:10:"DisableURI";O:8:"stdClass":3:{s:9:"namespace";s:3:"URI";s:4:"name";s:7:"Disable";s:7:"isAlias";b:1;}}s:10:"AutoFormat";a:4:{s:13:"AutoParagraph";i:7;s:6:"Custom";i:9;s:7:"Linkify";i:7;s:15:"PurifierLinkify";i:7;}s:15:"AutoFormatParam";a:1:{s:21:"PurifierLinkifyDocURL";i:1;}s:3:"CSS";a:6:{s:14:"AllowImportant";i:7;s:11:"AllowTricky";i:7;s:17:"AllowedProperties";i:-8;s:13:"DefinitionRev";i:5;s:12:"MaxImgLength";i:-1;s:11:"Proprietary";i:7;}s:5:"Cache";a:2:{s:14:"DefinitionImpl";i:-1;s:14:"SerializerPath";i:-1;}s:4:"Core";a:20:{s:15:"DefinitionCache";O:8:"stdClass":3:{s:9:"namespace";s:5:"Cache";s:4:"name";s:14:"DefinitionImpl";s:7:"isAlias";b:1;}s:17:"AggressivelyFixLt";i:7;s:13:"CollectErrors";i:7;s:13:"ColorKeywords";i:10;s:25:"ConvertDocumentToFragment";i:7;s:19:"AcceptFullDocuments";O:8:"stdClass":3:{s:9:"namespace";s:4:"Core";s:4:"name";s:25:"ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:31:"DirectLexLineNumberSyncInterval";i:5;s:8:"Encoding";i:2;s:21:"EscapeInvalidChildren";i:7;s:17:"EscapeInvalidTags";i:7;s:24:"EscapeNonASCIICharacters";i:7;s:14:"HiddenElements";i:8;s:8:"Language";i:1;s:9:"LexerImpl";i:-11;s:19:"MaintainLineNumbers";i:-7;s:16:"RemoveInvalidImg";i:7;s:20:"RemoveScriptContents";i:-7;s:5:"XHTML";O:8:"stdClass":3:{s:9:"namespace";s:4:"HTML";s:4:"name";s:5:"XHTML";s:7:"isAlias";b:1;}s:21:"CommentScriptContents";O:8:"stdClass":3:{s:9:"namespace";s:6:"Output";s:4:"name";s:21:"CommentScriptContents";s:7:"isAlias";b:1;}s:10:"TidyFormat";O:8:"stdClass":3:{s:9:"namespace";s:6:"Output";s:4:"name";s:10:"TidyFormat";s:7:"isAlias";b:1;}}s:6:"Filter";a:5:{s:6:"Custom";i:9;s:18:"ExtractStyleBlocks";i:7;s:7:"YouTube";i:7;s:26:"ExtractStyleBlocksEscaping";O:8:"stdClass":3:{s:9:"namespace";s:11:"FilterParam";s:4:"name";s:26:"ExtractStyleBlocksEscaping";s:7:"isAlias";b:1;}s:23:"ExtractStyleBlocksScope";O:8:"stdClass":3:{s:9:"namespace";s:11:"FilterParam";s:4:"name";s:23:"ExtractStyleBlocksScope";s:7:"isAlias";b:1;}}s:11:"FilterParam";a:3:{s:26:"ExtractStyleBlocksEscaping";i:7;s:23:"ExtractStyleBlocksScope";i:-1;s:26:"ExtractStyleBlocksTidyImpl";i:-11;}s:4:"HTML";a:22:{s:12:"EnableAttrID";O:8:"stdClass":3:{s:9:"namespace";s:4:"Attr";s:4:"name";s:8:"EnableID";s:7:"isAlias";b:1;}s:7:"Allowed";i:-4;s:17:"AllowedAttributes";i:-8;s:15:"AllowedElements";i:-8;s:14:"AllowedModules";i:-8;s:12:"BlockWrapper";i:1;s:11:"CoreModules";i:8;s:13:"CustomDoctype";i:-1;s:12:"DefinitionID";i:-1;s:13:"DefinitionRev";i:5;s:7:"Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:19:"ForbiddenAttributes";i:8;s:17:"ForbiddenElements";i:8;s:12:"MaxImgLength";i:-5;s:6:"Parent";i:1;s:11:"Proprietary";i:7;s:6:"Strict";i:7;s:7:"TidyAdd";i:8;s:9:"TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:10:"TidyRemove";i:8;s:7:"Trusted";i:7;s:5:"XHTML";i:7;}s:6:"Output";a:3:{s:21:"CommentScriptContents";i:7;s:7:"Newline";i:-1;s:10:"TidyFormat";i:7;}s:4:"Test";a:1:{s:12:"ForceNoIconv";i:7;}s:3:"URI";a:14:{s:14:"AllowedSchemes";i:8;s:4:"Base";i:-1;s:13:"DefaultScheme";i:1;s:12:"DefinitionID";i:-1;s:13:"DefinitionRev";i:5;s:7:"Disable";i:7;s:15:"DisableExternal";i:7;s:24:"DisableExternalResources";i:7;s:16:"DisableResources";i:7;s:4:"Host";i:-1;s:13:"HostBlacklist";i:9;s:12:"MakeAbsolute";i:7;s:5:"Munge";i:-1;s:22:"OverrideAllowedSchemes";i:7;}}} \ No newline at end of file +O:25:"HTMLPurifier_ConfigSchema":2:{s:8:"defaults";a:12:{s:4:"Attr";a:11:{s:19:"AllowedFrameTargets";a:0:{}s:10:"AllowedRel";a:0:{}s:10:"AllowedRev";a:0:{}s:19:"DefaultInvalidImage";s:0:"";s:22:"DefaultInvalidImageAlt";s:13:"Invalid image";s:14:"DefaultTextDir";s:3:"ltr";s:8:"EnableID";b:0;s:11:"IDBlacklist";a:0:{}s:17:"IDBlacklistRegexp";N;s:8:"IDPrefix";s:0:"";s:13:"IDPrefixLocal";s:0:"";}s:10:"AutoFormat";a:4:{s:13:"AutoParagraph";b:0;s:6:"Custom";a:0:{}s:7:"Linkify";b:0;s:15:"PurifierLinkify";b:0;}s:15:"AutoFormatParam";a:1:{s:21:"PurifierLinkifyDocURL";s:3:"#%s";}s:3:"CSS";a:6:{s:14:"AllowImportant";b:0;s:11:"AllowTricky";b:0;s:17:"AllowedProperties";N;s:13:"DefinitionRev";i:1;s:12:"MaxImgLength";s:6:"1200px";s:11:"Proprietary";b:0;}s:5:"Cache";a:2:{s:14:"DefinitionImpl";s:10:"Serializer";s:14:"SerializerPath";N;}s:4:"Core";a:15:{s:17:"AggressivelyFixLt";b:0;s:13:"CollectErrors";b:0;s:13:"ColorKeywords";a:17:{s:6:"maroon";s:7:"#800000";s:3:"red";s:7:"#FF0000";s:6:"orange";s:7:"#FFA500";s:6:"yellow";s:7:"#FFFF00";s:5:"olive";s:7:"#808000";s:6:"purple";s:7:"#800080";s:7:"fuchsia";s:7:"#FF00FF";s:5:"white";s:7:"#FFFFFF";s:4:"lime";s:7:"#00FF00";s:5:"green";s:7:"#008000";s:4:"navy";s:7:"#000080";s:4:"blue";s:7:"#0000FF";s:4:"aqua";s:7:"#00FFFF";s:4:"teal";s:7:"#008080";s:5:"black";s:7:"#000000";s:6:"silver";s:7:"#C0C0C0";s:4:"gray";s:7:"#808080";}s:25:"ConvertDocumentToFragment";b:1;s:31:"DirectLexLineNumberSyncInterval";i:0;s:8:"Encoding";s:5:"utf-8";s:21:"EscapeInvalidChildren";b:0;s:17:"EscapeInvalidTags";b:0;s:24:"EscapeNonASCIICharacters";b:0;s:14:"HiddenElements";a:2:{s:6:"script";b:1;s:5:"style";b:1;}s:8:"Language";s:2:"en";s:9:"LexerImpl";N;s:19:"MaintainLineNumbers";N;s:16:"RemoveInvalidImg";b:1;s:20:"RemoveScriptContents";N;}s:6:"Filter";a:3:{s:6:"Custom";a:0:{}s:18:"ExtractStyleBlocks";b:0;s:7:"YouTube";b:0;}s:11:"FilterParam";a:3:{s:26:"ExtractStyleBlocksEscaping";b:1;s:23:"ExtractStyleBlocksScope";N;s:26:"ExtractStyleBlocksTidyImpl";N;}s:4:"HTML";a:21:{s:7:"Allowed";N;s:17:"AllowedAttributes";N;s:15:"AllowedElements";N;s:14:"AllowedModules";N;s:12:"BlockWrapper";s:1:"p";s:11:"CoreModules";a:7:{s:9:"Structure";b:1;s:4:"Text";b:1;s:9:"Hypertext";b:1;s:4:"List";b:1;s:22:"NonXMLCommonAttributes";b:1;s:19:"XMLCommonAttributes";b:1;s:16:"CommonAttributes";b:1;}s:13:"CustomDoctype";N;s:12:"DefinitionID";N;s:13:"DefinitionRev";i:1;s:7:"Doctype";N;s:19:"ForbiddenAttributes";a:0:{}s:17:"ForbiddenElements";a:0:{}s:12:"MaxImgLength";i:1200;s:6:"Parent";s:3:"div";s:11:"Proprietary";b:0;s:6:"Strict";b:0;s:7:"TidyAdd";a:0:{}s:9:"TidyLevel";s:6:"medium";s:10:"TidyRemove";a:0:{}s:7:"Trusted";b:0;s:5:"XHTML";b:1;}s:6:"Output";a:3:{s:21:"CommentScriptContents";b:1;s:7:"Newline";N;s:10:"TidyFormat";b:0;}s:4:"Test";a:1:{s:12:"ForceNoIconv";b:0;}s:3:"URI";a:16:{s:14:"AllowedSchemes";a:6:{s:4:"http";b:1;s:5:"https";b:1;s:6:"mailto";b:1;s:3:"ftp";b:1;s:4:"nntp";b:1;s:4:"news";b:1;}s:4:"Base";N;s:13:"DefaultScheme";s:4:"http";s:12:"DefinitionID";N;s:13:"DefinitionRev";i:1;s:7:"Disable";b:0;s:15:"DisableExternal";b:0;s:24:"DisableExternalResources";b:0;s:16:"DisableResources";b:0;s:4:"Host";N;s:13:"HostBlacklist";a:0:{}s:12:"MakeAbsolute";b:0;s:5:"Munge";N;s:22:"OverrideAllowedSchemes";b:1;s:11:"SecureMunge";N;s:20:"SecureMungeSecretKey";N;}}s:4:"info";a:12:{s:4:"Attr";a:12:{s:19:"AllowedFrameTargets";i:8;s:10:"AllowedRel";i:8;s:10:"AllowedRev";i:8;s:19:"DefaultInvalidImage";i:1;s:22:"DefaultInvalidImageAlt";i:1;s:14:"DefaultTextDir";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:2:{s:3:"ltr";b:1;s:3:"rtl";b:1;}}s:8:"EnableID";i:7;s:11:"IDBlacklist";i:9;s:17:"IDBlacklistRegexp";i:-1;s:8:"IDPrefix";i:1;s:13:"IDPrefixLocal";i:1;s:10:"DisableURI";O:8:"stdClass":3:{s:9:"namespace";s:3:"URI";s:4:"name";s:7:"Disable";s:7:"isAlias";b:1;}}s:10:"AutoFormat";a:4:{s:13:"AutoParagraph";i:7;s:6:"Custom";i:9;s:7:"Linkify";i:7;s:15:"PurifierLinkify";i:7;}s:15:"AutoFormatParam";a:1:{s:21:"PurifierLinkifyDocURL";i:1;}s:3:"CSS";a:6:{s:14:"AllowImportant";i:7;s:11:"AllowTricky";i:7;s:17:"AllowedProperties";i:-8;s:13:"DefinitionRev";i:5;s:12:"MaxImgLength";i:-1;s:11:"Proprietary";i:7;}s:5:"Cache";a:2:{s:14:"DefinitionImpl";i:-1;s:14:"SerializerPath";i:-1;}s:4:"Core";a:20:{s:15:"DefinitionCache";O:8:"stdClass":3:{s:9:"namespace";s:5:"Cache";s:4:"name";s:14:"DefinitionImpl";s:7:"isAlias";b:1;}s:17:"AggressivelyFixLt";i:7;s:13:"CollectErrors";i:7;s:13:"ColorKeywords";i:10;s:25:"ConvertDocumentToFragment";i:7;s:19:"AcceptFullDocuments";O:8:"stdClass":3:{s:9:"namespace";s:4:"Core";s:4:"name";s:25:"ConvertDocumentToFragment";s:7:"isAlias";b:1;}s:31:"DirectLexLineNumberSyncInterval";i:5;s:8:"Encoding";i:2;s:21:"EscapeInvalidChildren";i:7;s:17:"EscapeInvalidTags";i:7;s:24:"EscapeNonASCIICharacters";i:7;s:14:"HiddenElements";i:8;s:8:"Language";i:1;s:9:"LexerImpl";i:-11;s:19:"MaintainLineNumbers";i:-7;s:16:"RemoveInvalidImg";i:7;s:20:"RemoveScriptContents";i:-7;s:5:"XHTML";O:8:"stdClass":3:{s:9:"namespace";s:4:"HTML";s:4:"name";s:5:"XHTML";s:7:"isAlias";b:1;}s:21:"CommentScriptContents";O:8:"stdClass":3:{s:9:"namespace";s:6:"Output";s:4:"name";s:21:"CommentScriptContents";s:7:"isAlias";b:1;}s:10:"TidyFormat";O:8:"stdClass":3:{s:9:"namespace";s:6:"Output";s:4:"name";s:10:"TidyFormat";s:7:"isAlias";b:1;}}s:6:"Filter";a:5:{s:6:"Custom";i:9;s:18:"ExtractStyleBlocks";i:7;s:7:"YouTube";i:7;s:26:"ExtractStyleBlocksEscaping";O:8:"stdClass":3:{s:9:"namespace";s:11:"FilterParam";s:4:"name";s:26:"ExtractStyleBlocksEscaping";s:7:"isAlias";b:1;}s:23:"ExtractStyleBlocksScope";O:8:"stdClass":3:{s:9:"namespace";s:11:"FilterParam";s:4:"name";s:23:"ExtractStyleBlocksScope";s:7:"isAlias";b:1;}}s:11:"FilterParam";a:3:{s:26:"ExtractStyleBlocksEscaping";i:7;s:23:"ExtractStyleBlocksScope";i:-1;s:26:"ExtractStyleBlocksTidyImpl";i:-11;}s:4:"HTML";a:22:{s:12:"EnableAttrID";O:8:"stdClass":3:{s:9:"namespace";s:4:"Attr";s:4:"name";s:8:"EnableID";s:7:"isAlias";b:1;}s:7:"Allowed";i:-4;s:17:"AllowedAttributes";i:-8;s:15:"AllowedElements";i:-8;s:14:"AllowedModules";i:-8;s:12:"BlockWrapper";i:1;s:11:"CoreModules";i:8;s:13:"CustomDoctype";i:-1;s:12:"DefinitionID";i:-1;s:13:"DefinitionRev";i:5;s:7:"Doctype";O:8:"stdClass":3:{s:4:"type";i:1;s:10:"allow_null";b:1;s:7:"allowed";a:5:{s:22:"HTML 4.01 Transitional";b:1;s:16:"HTML 4.01 Strict";b:1;s:22:"XHTML 1.0 Transitional";b:1;s:16:"XHTML 1.0 Strict";b:1;s:9:"XHTML 1.1";b:1;}}s:19:"ForbiddenAttributes";i:8;s:17:"ForbiddenElements";i:8;s:12:"MaxImgLength";i:-5;s:6:"Parent";i:1;s:11:"Proprietary";i:7;s:6:"Strict";i:7;s:7:"TidyAdd";i:8;s:9:"TidyLevel";O:8:"stdClass":2:{s:4:"type";i:1;s:7:"allowed";a:4:{s:4:"none";b:1;s:5:"light";b:1;s:6:"medium";b:1;s:5:"heavy";b:1;}}s:10:"TidyRemove";i:8;s:7:"Trusted";i:7;s:5:"XHTML";i:7;}s:6:"Output";a:3:{s:21:"CommentScriptContents";i:7;s:7:"Newline";i:-1;s:10:"TidyFormat";i:7;}s:4:"Test";a:1:{s:12:"ForceNoIconv";i:7;}s:3:"URI";a:16:{s:14:"AllowedSchemes";i:8;s:4:"Base";i:-1;s:13:"DefaultScheme";i:1;s:12:"DefinitionID";i:-1;s:13:"DefinitionRev";i:5;s:7:"Disable";i:7;s:15:"DisableExternal";i:7;s:24:"DisableExternalResources";i:7;s:16:"DisableResources";i:7;s:4:"Host";i:-1;s:13:"HostBlacklist";i:9;s:12:"MakeAbsolute";i:7;s:5:"Munge";i:-1;s:22:"OverrideAllowedSchemes";i:7;s:11:"SecureMunge";i:-1;s:20:"SecureMungeSecretKey";i:-1;}}} \ No newline at end of file diff --git a/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMunge.txt b/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMunge.txt new file mode 100644 index 00000000..68860ae4 --- /dev/null +++ b/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMunge.txt @@ -0,0 +1,33 @@ +URI.SecureMunge +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

+ 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: +

+ +
$checksum === sha1($secret_key . ':' . $url)
+ +

+ If the output is TRUE, the redirector script should accept the URI. +

+ +

+ 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. +

+ +

+ This is a post-filter. This filter may conflict with other + post-filters that deal with external links. +

\ No newline at end of file diff --git a/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMungeSecretKey.txt b/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMungeSecretKey.txt new file mode 100644 index 00000000..05244e56 --- /dev/null +++ b/library/HTMLPurifier/ConfigSchema/schema/URI.SecureMungeSecretKey.txt @@ -0,0 +1,11 @@ +URI.SecureMungeSecretKey +TYPE: string/null +VERSION: 3.1.1 +DEFAULT: NULL +--DESCRIPTION-- +

+ 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. +

\ No newline at end of file diff --git a/library/HTMLPurifier/URIDefinition.php b/library/HTMLPurifier/URIDefinition.php index 390d97ec..1314ed3c 100644 --- a/library/HTMLPurifier/URIDefinition.php +++ b/library/HTMLPurifier/URIDefinition.php @@ -5,6 +5,7 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition public $type = 'URI'; protected $filters = array(); + protected $postFilters = array(); protected $registeredFilters = array(); /** @@ -34,8 +35,13 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition } public function addFilter($filter, $config) { - $filter->prepare($config); - $this->filters[$filter->name] = $filter; + $r = $filter->prepare($config); + 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) { @@ -66,8 +72,16 @@ class HTMLPurifier_URIDefinition extends HTMLPurifier_Definition } public function filter(&$uri, $config, $context) { - foreach ($this->filters as $name => $x) { - $result = $this->filters[$name]->filter($uri, $config, $context); + foreach ($this->filters as $name => $f) { + $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; } return true; diff --git a/library/HTMLPurifier/URIFilter.php b/library/HTMLPurifier/URIFilter.php index 0108258f..6953ed43 100644 --- a/library/HTMLPurifier/URIFilter.php +++ b/library/HTMLPurifier/URIFilter.php @@ -19,10 +19,15 @@ abstract class HTMLPurifier_URIFilter */ public $name; + /** + * True if this filter should be run after scheme validation. + */ + public $post = false; + /** * Performs initialization for the filter */ - public function prepare($config) {} + public function prepare($config) {return true;} /** * Filter a URI object diff --git a/library/HTMLPurifier/URIFilter/HostBlacklist.php b/library/HTMLPurifier/URIFilter/HostBlacklist.php index bd719701..01320479 100644 --- a/library/HTMLPurifier/URIFilter/HostBlacklist.php +++ b/library/HTMLPurifier/URIFilter/HostBlacklist.php @@ -6,6 +6,7 @@ class HTMLPurifier_URIFilter_HostBlacklist extends HTMLPurifier_URIFilter protected $blacklist = array(); public function prepare($config) { $this->blacklist = $config->get('URI', 'HostBlacklist'); + return true; } public function filter(&$uri, $config, $context) { foreach($this->blacklist as $blacklisted_host_fragment) { diff --git a/library/HTMLPurifier/URIFilter/MakeAbsolute.php b/library/HTMLPurifier/URIFilter/MakeAbsolute.php index 647f779e..289db51a 100644 --- a/library/HTMLPurifier/URIFilter/MakeAbsolute.php +++ b/library/HTMLPurifier/URIFilter/MakeAbsolute.php @@ -11,14 +11,15 @@ class HTMLPurifier_URIFilter_MakeAbsolute extends HTMLPurifier_URIFilter $def = $config->getDefinition('URI'); $this->base = $def->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); - return; + trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING); + return false; } $this->base->fragment = null; // fragment is invalid for base URI $stack = explode('/', $this->base->path); array_pop($stack); // discard last segment $stack = $this->_collapseStack($stack); // do pre-parsing $this->basePathStack = $stack; + return true; } public function filter(&$uri, $config, $context) { if (is_null($this->base)) return true; // abort early diff --git a/library/HTMLPurifier/URIFilter/SecureMunge.php b/library/HTMLPurifier/URIFilter/SecureMunge.php new file mode 100644 index 00000000..fe74ac53 --- /dev/null +++ b/library/HTMLPurifier/URIFilter/SecureMunge.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/tests/HTMLPurifier/AttrDef/URITest.php b/tests/HTMLPurifier/AttrDef/URITest.php index a22adaab..8e65c73b 100644 --- a/tests/HTMLPurifier/AttrDef/URITest.php +++ b/tests/HTMLPurifier/AttrDef/URITest.php @@ -83,6 +83,8 @@ class HTMLPurifier_AttrDef_URITest extends HTMLPurifier_AttrDefHarness $uri_def = new HTMLPurifier_URIDefinitionMock(); $uri_def->expectOnce('filter', 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; // Since definitions are no longer passed by reference, we need diff --git a/tests/HTMLPurifier/URIDefinitionTest.php b/tests/HTMLPurifier/URIDefinitionTest.php index a418a589..1afb3a42 100644 --- a/tests/HTMLPurifier/URIDefinitionTest.php +++ b/tests/HTMLPurifier/URIDefinitionTest.php @@ -3,14 +3,16 @@ 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; generate_mock_once('HTMLPurifier_URIFilter'); $mock = new HTMLPurifier_URIFilterMock(); if ($expect) $mock->expectOnce('filter'); else $mock->expectNever('filter'); $mock->setReturnValue('filter', $result); + $mock->setReturnValue('prepare', $setup); $mock->name = $i++; + $mock->post = $post; return $mock; } diff --git a/tests/HTMLPurifier/URIFilter/SecureMungeTest.php b/tests/HTMLPurifier/URIFilter/SecureMungeTest.php new file mode 100644 index 00000000..dd55dd5e --- /dev/null +++ b/tests/HTMLPurifier/URIFilter/SecureMungeTest.php @@ -0,0 +1,49 @@ +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'); + } + +}