From 042c8bc3cc26d43fa066a2d89d60fa95e3c9c4fc Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sat, 14 Feb 2015 19:32:57 +0000 Subject: [PATCH] move code for html or javascript content-type headers from webdav to html class and use it for attachments too --- mail/inc/class.mail_compose.inc.php | 2 +- mail/inc/class.mail_ui.inc.php | 13 +-- phpgwapi/inc/class.html.inc.php | 109 ++++++++++++++++--- phpgwapi/inc/class.vfs_webdav_server.inc.php | 55 ++-------- 4 files changed, 107 insertions(+), 72 deletions(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index bfe0150902..b4c979d100 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -1870,7 +1870,7 @@ class mail_compose } } //error_log(__METHOD__.__LINE__.'->'.array2string($attachment)); - html::content_header($attachment['name'], $attachment['type'], 0, True, $_GET['mode'] == "save"); + html::safe_content_header($attachment['attachment'], $attachment['name'], $attachment['type'], $size=0, true, $_GET['mode'] == "save"); echo $attachment['attachment']; common::egw_exit(); diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php index 11417cc653..44a9d308be 100644 --- a/mail/inc/class.mail_ui.inc.php +++ b/mail/inc/class.mail_ui.inc.php @@ -2506,7 +2506,7 @@ class mail_ui } //error_log(__METHOD__.__LINE__.'->'.array2string($attachment)); $filename = ($attachment['name']?$attachment['name']:($attachment['filename']?$attachment['filename']:$mailbox.'_uid'.$uid.'_part'.$part)); - html::content_header($filename,$attachment['type'],0,True,($_GET['mode'] == "save")); + html::safe_content_header($attachment['attachment'], $filename, $attachment['type'], $size=0, True, $_GET['mode'] == "save"); echo $attachment['attachment']; $GLOBALS['egw']->common->egw_exit(); @@ -2550,19 +2550,16 @@ class mail_ui } $GLOBALS['egw']->session->commit_session(); - if ($display==false) + if (!$display) { $subject = str_replace('$$','__',mail_bo::decode_header($headers['SUBJECT'])); - html::content_header($subject .".eml",'message/rfc822',0,True,($display==false)); + html::safe_content_header($message, $subject.".eml", 'message/rfc822', $size=0, true, true); echo $message; - - $GLOBALS['egw']->common->egw_exit(); - exit; } else { - header('Content-type: text/html; charset=iso-8859-1'); - print '
'. htmlspecialchars($message, ENT_NOQUOTES, 'iso-8859-1') .'
'; + html::safe_content_header($message, $subject.".eml", 'text/html', $size=0, true, false); + print '
'. htmlspecialchars($message, ENT_NOQUOTES, 'utf-8') .'
'; } } diff --git a/phpgwapi/inc/class.html.inc.php b/phpgwapi/inc/class.html.inc.php index 20bdbb0edf..31e26fc912 100644 --- a/phpgwapi/inc/class.html.inc.php +++ b/phpgwapi/inc/class.html.inc.php @@ -1488,7 +1488,91 @@ egw_LAB.wait(function() { } /** - * Output content headers for file downloads + * Output content headers for user-content, mitigating risk of javascript or html + * + * Mitigate risk of serving javascript or css from our domain, + * which will get around same origin policy and CSP! + * + * Mitigate risk of html downloads by using CSP or force download for IE + * + * @param resource|string &$content content might be changed by this call + * @param string $path filename or path for content-disposition header + * @param string &$mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() + * on return used, maybe changed, mime-type + * @param int $length =0 content length, default 0 = skip that header + * on return changed size + * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download + * @param boolean $force_download =true send content-disposition attachment header + * @param boolean $no_content_type =false do not send actual content-type and content-length header, just content-disposition + */ + public static function safe_content_header(&$content, $path, &$mime='', &$length=0, $nocache=true, $force_download=true, $no_content_type=false) + { + // mitigate risk of serving javascript or css via webdav from our domain, + // which will get around same origin policy and CSP + list($type, $subtype) = explode('/', strtolower($mime)); + if (!$force_download && in_array($type, array('application', 'text')) && + in_array($subtype, array('javascript', 'x-javascript', 'ecmascript', 'jscript', 'vbscript', 'css'))) + { + // unfortunatly only Chrome and IE >= 8 allow to switch content-sniffing off with X-Content-Type-Options: nosniff + if (html::$user_agent == 'chrome' || html::$user_agent == 'msie' && html::$ua_version >= 8) + { + $mime = 'text/plain'; + header('X-Content-Type-Options: nosniff'); // stop IE & Chrome from content-type sniffing + } + // for the rest we change mime-type to text/html and let code below handle it safely + // this stops Safari and Firefox from using it as src attribute in a script tag + // but only for "real" browsers, we dont want to modify data for our WebDAV clients + elseif (isset($_SERVER['HTTP_REFERER'])) + { + $mime = 'text/html'; + if (is_resource($content)) + { + $data = fread($content, $length); + fclose($content); + $content =& $data; + unset($data); + } + $content = '
'.$content;
+				$length += 5;
+			}
+		}
+		// mitigate risk of html downloads by using CSP or force download for IE
+		if (!$force_download && in_array($mime, array('text/html', 'application/xhtml+xml')))
+		{
+			// use CSP only for current user-agents/versions I was able to positivly test
+			if (html::$user_agent == 'chrome' && html::$ua_version >= 24 ||
+				// mobile FF 24 on Android does NOT honor CSP!
+				html::$user_agent == 'firefox' && !html::$ua_mobile && html::$ua_version >= 24 ||
+				html::$user_agent == 'safari' && !html::$ua_mobile && html::$ua_version >= 536 ||	// OS X
+				html::$user_agent == 'safari' && html::$ua_mobile && html::$ua_version >= 9537)	// iOS 7
+			{
+				$csp = "script-src 'none'";	// forbid to execute any javascript
+				header("Content-Security-Policy: $csp");
+				header("X-Webkit-CSP: $csp");	// Chrome: <= 24, Safari incl. iOS
+				//header("X-Content-Security-Policy: $csp");	// FF <= 22
+				//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-Security-Policy: $csp");
+			}
+			else	// everything else get's a Content-dispostion: attachment, to be on save side
+			{
+				//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-disposition: attachment");
+				$force_download = true;
+			}
+		}
+		if ($no_content_type)
+		{
+			if ($force_download) self::content_disposition_header(egw_vfs::basename($path), $force_download);
+		}
+		else
+		{
+			self::content_header(egw_vfs::basename($path), $mime, $length, $nocache, $force_download);
+		}
+	}
+
+	/**
+	 * Output content-type headers for file downloads
+	 *
+	 * This function should only be used for non-user supplied content!
+	 * For uploaded files, mail attachmentes, etc, you have to use safe_content_header!
 	 *
 	 * @author Miles Lott originally in browser class
 	 * @param string $fn filename
@@ -1533,21 +1617,16 @@ egw_LAB.wait(function() {
 	 */
 	public static function content_disposition_header($fn,$forceDownload=true)
 	{
-			if ($forceDownload===true)
-			{
-				$attachment = ' attachment;';
-			}
-			else
-			{
-				$attachment = ' inline;';
-			}
-			// limit IE hack (no attachment in Content-disposition header) to IE < 9
-			if(self::$user_agent == 'msie' && self::$ua_version < 9)
-			{
-				$attachment = '';
-			}
+		if ($forceDownload===true)
+		{
+			$attachment = ' attachment;';
+		}
+		else
+		{
+			$attachment = ' inline;';
+		}
 
-			header('Content-disposition:'.$attachment.' filename="'.translation::to_ascii($fn).'"; filename*=utf-8\'\''.rawurlencode($fn));
+		header('Content-disposition:'.$attachment.' filename="'.translation::to_ascii($fn).'"; filename*=utf-8\'\''.rawurlencode($fn));
 	}
 
 	/**
diff --git a/phpgwapi/inc/class.vfs_webdav_server.inc.php b/phpgwapi/inc/class.vfs_webdav_server.inc.php
index 8fe8b55866..8e9c7f080a 100644
--- a/phpgwapi/inc/class.vfs_webdav_server.inc.php
+++ b/phpgwapi/inc/class.vfs_webdav_server.inc.php
@@ -648,55 +648,14 @@ class vfs_webdav_server extends HTTP_WebDAV_Server_Filesystem
 		}
 		if (($ok = parent::GET($options)))
 		{
-			// mitigate risk of serving javascript or css via webdav from our domain,
-			// which will get around same origin policy and CSP
-			list($type, $subtype) = explode('/', strtolower($options['mimetype']));
-			if (!$this->force_download && in_array($type, array('application', 'text')) &&
-				in_array($subtype, array('javascript', 'x-javascript', 'ecmascript', 'jscript', 'vbscript', 'css')))
+			// mitigate risks of serving javascript or css from our domain
+			html::safe_content_header($options['stream'], $options['path'], $options['mimetype'], $options['size'], false,
+				$this->force_download, true);	// true = do not send content-type and content-length header, but modify values
+
+			if (!is_stream($options['stream']))
 			{
-				// unfortunatly only Chrome and IE >= 8 allow to switch content-sniffing off with X-Content-Type-Options: nosniff
-				if (html::$user_agent == 'chrome' || html::$user_agent == 'msie' && html::$ua_version >= 8)
-				{
-					$options['mimetype'] = 'text/plain';
-					header('X-Content-Type-Options: nosniff');	// stop IE & Chrome from content-type sniffing
-				}
-				// for the rest we change mime-type to text/html and let code below handle it safely
-				// this stops Safari and Firefox from using it as src attribute in a script tag
-				// but only for "real" browsers, we dont want to modify data for our WebDAV clients
-				elseif (isset($_SERVER['HTTP_REFERER']))
-				{
-					$options['mimetype'] = 'text/html';
-					$options['data'] = '
'.fread($options['stream'], $options['size']);
-					$options['size'] += 5;
-					fclose($options['stream']);
-					unset($options['stream']);
-				}
-			}
-			// mitigate risk of html downloads by using CSP or force download for IE
-			if (!$this->force_download && in_array($options['mimetype'], array('text/html', 'application/xhtml+xml')))
-			{
-				// use CSP only for current user-agents/versions I was able to positivly test
-				if (html::$user_agent == 'chrome' && html::$ua_version >= 24 ||
-					// mobile FF 24 on Android does NOT honor CSP!
-					html::$user_agent == 'firefox' && !html::$ua_mobile && html::$ua_version >= 24 ||
-					html::$user_agent == 'safari' && !html::$ua_mobile && html::$ua_version >= 536 ||	// OS X
-					html::$user_agent == 'safari' && html::$ua_mobile && html::$ua_version >= 9537)	// iOS 7
-				{
-					$csp = "script-src 'none'";	// forbid to execute any javascript
-					header("Content-Security-Policy: $csp");
-					header("X-Webkit-CSP: $csp");	// Chrome: <= 24, Safari incl. iOS
-					//header("X-Content-Security-Policy: $csp");	// FF <= 22
-					//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-Security-Policy: $csp");
-				}
-				else	// everything else get's a Content-dispostion: attachment, to be on save side
-				{
-					//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-disposition: attachment");
-					$this->force_download = true;
-				}
-			}
-			if ($this->force_download)
-			{
-				html::content_disposition_header(egw_vfs::basename($options['path']),true);
+				$options['data'] =& $options['stream'];
+				unset($options['stream']);
 			}
 		}
 		return $ok;