]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/clean.php
Release 6.5.16
[Github/sugarcrm.git] / include / clean.php
1 <?php
2 /*********************************************************************************
3  * SugarCRM Community Edition is a customer relationship management program developed by
4  * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
5  * 
6  * This program is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU Affero General Public License version 3 as published by the
8  * Free Software Foundation with the addition of the following permission added
9  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10  * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
11  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12  * 
13  * This program is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
16  * details.
17  * 
18  * You should have received a copy of the GNU Affero General Public License along with
19  * this program; if not, see http://www.gnu.org/licenses or write to the Free
20  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21  * 02110-1301 USA.
22  * 
23  * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
24  * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
25  * 
26  * The interactive user interfaces in modified source and object code versions
27  * of this program must display Appropriate Legal Notices, as required under
28  * Section 5 of the GNU Affero General Public License version 3.
29  * 
30  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31  * these Appropriate Legal Notices must retain the display of the "Powered by
32  * SugarCRM" logo. If the display of the logo is not reasonably feasible for
33  * technical reasons, the Appropriate Legal Notices must display the words
34  * "Powered by SugarCRM".
35  ********************************************************************************/
36
37
38 require_once 'include/HTMLPurifier/HTMLPurifier.standalone.php';
39 require_once 'include/HTMLPurifier/HTMLPurifier.autoload.php';
40
41 /**
42  * cid: scheme implementation
43  */
44 class HTMLPurifier_URIScheme_cid extends HTMLPurifier_URIScheme
45 {
46     public $browsable = true;
47     public $may_omit_host = true;
48
49     public function doValidate(&$uri, $config, $context) {
50         $uri->userinfo = null;
51         $uri->port     = null;
52         $uri->host     = null;
53         $uri->query    = null;
54         $uri->fragment = null;
55         return true;
56     }
57
58 }
59
60 class HTMLPurifier_Filter_Xmp extends HTMLPurifier_Filter
61 {
62
63     public $name = 'Xmp';
64
65     public function preFilter($html, $config, $context)
66     {
67         return preg_replace("#<(/)?xmp>#i", "<\\1pre>", $html);
68     }
69 }
70
71 class SugarCleaner
72 {
73     /**
74      * Singleton instance
75      * @var SugarCleaner
76      */
77     static public $instance;
78
79     /**
80      * HTMLPurifier instance
81      * @var HTMLPurifier
82      */
83     protected $purifier;
84
85     function __construct()
86     {
87         global $sugar_config;
88         $config = HTMLPurifier_Config::createDefault();
89
90         if(!is_dir(sugar_cached("htmlclean"))) {
91             create_cache_directory("htmlclean/");
92         }
93         $config->set('HTML.Doctype', 'XHTML 1.0 Transitional');
94         $config->set('Core.Encoding', 'UTF-8');
95         $hidden_tags = array('script' => true, 'style' => true, 'title' => true, 'head' => true);
96         $config->set('Core.HiddenElements', $hidden_tags);
97         $config->set('Cache.SerializerPath', sugar_cached("htmlclean"));
98         $config->set('URI.Base', $sugar_config['site_url']);
99         $config->set('CSS.Proprietary', true);
100         $config->set('HTML.TidyLevel', 'light');
101         $config->set('HTML.ForbiddenElements', array('body' => true, 'html' => true));
102         $config->set('AutoFormat.RemoveEmpty', false);
103         $config->set('Cache.SerializerPermissions', 0775);
104         // for style
105         //$config->set('Filter.ExtractStyleBlocks', true);
106         $config->set('Filter.ExtractStyleBlocks.TidyImpl', false); // can't use csstidy, GPL
107         if(!empty($GLOBALS['sugar_config']['html_allow_objects'])) {
108             // for object
109             $config->set('HTML.SafeObject', true);
110             // for embed
111             $config->set('HTML.SafeEmbed', true);
112         }
113         $config->set('Output.FlashCompat', true);
114         // for iframe and xmp
115         $config->set('Filter.Custom',  array(new HTMLPurifier_Filter_Xmp()));
116         // for link
117         $config->set('HTML.DefinitionID', 'Sugar HTML Def');
118         $config->set('HTML.DefinitionRev', 2);
119         $config->set('Cache.SerializerPath', sugar_cached('htmlclean/'));
120         // IDs are namespaced
121         $config->set('Attr.EnableID', true);
122         $config->set('Attr.IDPrefix', 'sugar_text_');
123
124         if ($def = $config->maybeGetRawHTMLDefinition()) {
125             $form = $def->addElement(
126                         'link',   // name
127                         'Flow',  // content set
128                         'Empty', // allowed children
129                         'Core', // attribute collection
130                  array( // attributes
131                         'href*' => 'URI',
132                         'rel' => 'Enum#stylesheet', // only stylesheets supported here
133                         'type' => 'Enum#text/css' // only CSS supported here
134                         )
135             );
136             $iframe = $def->addElement(
137                         'iframe',   // name
138                         'Flow',  // content set
139                         'Optional: #PCDATA | Flow | Block', // allowed children
140                         'Core', // attribute collection
141                  array( // attributes
142                         'src*' => 'URI',
143                     'frameborder' => 'Enum#0,1',
144                     'marginwidth' =>  'Pixels',
145                     'marginheight' =>  'Pixels',
146                     'scrolling' => 'Enum#|yes,no,auto',
147                         'align' => 'Enum#top,middle,bottom,left,right,center',
148                     'height' => 'Length',
149                     'width' => 'Length',
150                  )
151             );
152             $iframe->excludes=array('iframe');
153         }
154         $uri = $config->getDefinition('URI');
155         $uri->addFilter(new SugarURIFilter(), $config);
156         HTMLPurifier_URISchemeRegistry::instance()->register('cid', new HTMLPurifier_URIScheme_cid());
157
158         $this->purifier = new HTMLPurifier($config);
159     }
160
161     /**
162      * Get cleaner instance
163      * @return SugarCleaner
164      */
165     public static function getInstance()
166     {
167         if(is_null(self::$instance)) {
168             self::$instance = new self;
169         }
170         return self::$instance;
171     }
172
173     /**
174      * Clean string from potential XSS problems
175      * @param string $html
176      * @param bool $encoded Was it entity-encoded?
177      * @return string
178      */
179     static public function cleanHtml($html, $encoded = false)
180     {
181         if(empty($html)) return $html;
182
183         if($encoded) {
184             $html = from_html($html);
185         }
186         if(!preg_match('<[^-A-Za-z0-9 `~!@#$%^&*()_=+{}\[\];:\'",./\\?\r\n|\x80-\xFF]>', $html)) {
187             /* if it only has "safe" chars, don't bother */
188             $cleanhtml = $html;
189         } else {
190             $purifier = self::getInstance()->purifier;
191             $cleanhtml = $purifier->purify($html);
192 //            $styles = $purifier->context->get('StyleBlocks');
193 //            if(count($styles) > 0) {
194 //                $cleanhtml = "<style>".join("</style><style>", $styles)."</style>".$cleanhtml;
195 //            }
196         }
197         if($encoded) {
198             $cleanhtml = to_html($cleanhtml);
199         }
200         return $cleanhtml;
201     }
202
203     static public function stripTags($string, $encoded = true)
204     {
205         if($encoded) {
206             $string = from_html($string);
207         }
208         $string = filter_var($string, FILTER_SANITIZE_STRIPPED, FILTER_FLAG_NO_ENCODE_QUOTES);
209         return $encoded?to_html($string):$string;
210     }
211 }
212
213 /**
214  * URI filter for HTMLPurifier
215  * Approves only resource URIs that are in the list of trusted domains
216  * Until we have comprehensive CSRF protection, we need to sanitize URLs in emails, etc.
217  * to avoid CSRF attacks.
218  */
219 class SugarURIFilter extends HTMLPurifier_URIFilter
220 {
221     public $name = 'SugarURIFilter';
222 //    public $post = true;
223     protected $allowed = array();
224
225     public function prepare($config)
226     {
227         global $sugar_config;
228         if(!empty($sugar_config['security_trusted_domains']) && is_array($sugar_config['security_trusted_domains']))
229         {
230             $this->allowed = $sugar_config['security_trusted_domains'];
231         }
232         /* Allow this host?
233         $def = $config->getDefinition('URI');
234         if(!empty($def->base) && !empty($this->base->host)) {
235             $this->allowed[] = $def->base->host;
236         }
237         */
238     }
239
240     public function filter(&$uri, $config, $context)
241     {
242         // skip non-resource URIs
243         if (!$context->get('EmbeddedURI', true)) return true;
244
245         //if(empty($this->allowed)) return false;
246
247         if(!empty($uri->scheme) && strtolower($uri->scheme) != 'http' && strtolower($uri->scheme) != 'https') {
248                 // do not touch non-HTTP URLs
249                 return true;
250             }
251
252         // relative URLs permitted since email templates use it
253                 // if(empty($uri->host)) return false;
254             // allow URLs with no query
255                 if(empty($uri->query)) return true;
256
257                 // allow URLs for known good hosts
258                 foreach($this->allowed as $allow) {
259             // must be equal to our domain or subdomain of our domain
260             if($uri->host == $allow || substr($uri->host, -(strlen($allow)+1)) == ".$allow") {
261                 return true;
262             }
263         }
264
265         // Here we try to block URLs that may be used for nasty XSRF stuff by
266         // referring back to Sugar URLs
267         // allow URLs that don't start with /? or /index.php?
268                 if(!empty($uri->path) && $uri->path != '/') {
269                     $lpath = strtolower($uri->path);
270                     if(substr($lpath, -10) != '/index.php' && $lpath != 'index.php') {
271                         return true;
272                 }
273                 }
274
275         $query_items = array();
276                 parse_str(from_html($uri->query), $query_items);
277             // weird query, probably harmless
278                 if(empty($query_items)) return true;
279         // suspiciously like SugarCRM query, reject
280                 if(!empty($query_items['module']) && !empty($query_items['action'])) return false;
281         // looks like non-download entry point - allow only specific entry points
282                 if(!empty($query_items['entryPoint']) && !in_array($query_items['entryPoint'], array('download', 'image', 'getImage'))) {
283                         return false;
284                 }
285
286                 return true;
287     }
288 }