From 4f7c4dcba62fc29db043c82b12736697d5abb454 Mon Sep 17 00:00:00 2001
From: JediKev <kevin@enhancesoft.com>
Date: Thu, 7 Mar 2019 16:19:43 -0600
Subject: [PATCH] iframe: Allow Multiple iFrame Domains

Previously, we added a security header to prevent click-jacking called
"X-Frame-Options". This introduced an issue with people using osTicket in
iFrames on their websites. To mitigate the issue, this updates the security
header to allow the site to be framed from specified domains, if none
provided we default to 'self'. This adds a new field to General System
Settings called "Allow iFrames" where you may enter a comma separated list
of domains that the site can be framed on. This also adds a validator for
the field to validate the domains and ensure they fit the <host-source>
syntax from [Mozilla Developer
Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors#Sources).
---
 include/class.config.php                          |  6 ++++++
 include/class.validator.php                       |  8 ++++++++
 include/client/header.inc.php                     |  2 +-
 include/i18n/en_US/help/tips/settings.system.yaml | 14 ++++++++++++++
 include/staff/header.inc.php                      |  2 +-
 include/staff/login.header.php                    |  2 +-
 include/staff/settings-system.inc.php             |  7 +++++++
 setup/inc/header.inc.php                          |  4 +++-
 8 files changed, 41 insertions(+), 4 deletions(-)

diff --git a/include/class.config.php b/include/class.config.php
index 32f5ca702..0f046d345 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -429,6 +429,10 @@ class OsticketConfig extends Config {
         return $this->get('enable_richtext');
     }
 
+    function getAllowIframes() {
+        return str_replace(array(', ', ','), array(' ', ' '), $this->get('allow_iframes')) ?: 'self';
+    }
+
     function isAvatarsEnabled() {
         return $this->get('enable_avatars');
     }
@@ -1121,6 +1125,7 @@ class OsticketConfig extends Config {
         $f['helpdesk_title']=array('type'=>'string',   'required'=>1, 'error'=>__('Helpdesk title is required'));
         $f['default_dept_id']=array('type'=>'int',   'required'=>1, 'error'=>__('Default Department is required'));
         $f['autolock_minutes']=array('type'=>'int',   'required'=>1, 'error'=>__('Enter lock time in minutes'));
+        $f['allow_iframes']=array('type'=>'cs-domain',   'required'=>0, 'error'=>__('Enter comma separated list of domains'));
         //Date & Time Options
         $f['time_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Time format is required'));
         $f['date_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Date format is required'));
@@ -1179,6 +1184,7 @@ class OsticketConfig extends Config {
             'enable_avatars' => isset($vars['enable_avatars']) ? 1 : 0,
             'enable_richtext' => isset($vars['enable_richtext']) ? 1 : 0,
             'files_req_auth' => isset($vars['files_req_auth']) ? 1 : 0,
+            'allow_iframes' => Format::sanitize($vars['allow_iframes']),
         ));
     }
 
diff --git a/include/class.validator.php b/include/class.validator.php
index 14be7ccaf..388d00ac9 100644
--- a/include/class.validator.php
+++ b/include/class.validator.php
@@ -123,6 +123,14 @@ class Validator {
                 if(!is_numeric($this->input[$k]) || (strlen($this->input[$k])!=5))
                     $this->errors[$k]=$field['error'];
                 break;
+            case 'cs-domain': // Comma separated list of domains
+                if($values=explode(',', $this->input[$k]))
+                    foreach($values as $v)
+                        if(!preg_match_all(
+                                '/^(https?:\/\/)?((\*\.|\w+\.)?[\w-]+(\.[a-zA-Z]+)?(:([0-9]+|\*))?)+$/',
+                                ltrim($v)))
+                            $this->errors[$k]=$field['error'];
+                break;
             default://If param type is not set...or handle..error out...
                 $this->errors[$k]=$field['error'].' '.__('(type not set)');
             endswitch;
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index 0d66f6753..8c248b158 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -6,7 +6,7 @@ $signin_url = ROOT_PATH . "login.php"
 $signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken();
 
 header("Content-Type: text/html; charset=UTF-8");
-header("X-Frame-Options: SAMEORIGIN");
+header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';");
 if (($lang = Internationalization::getCurrentLanguage())) {
     $langs = array_unique(array($lang, $cfg->getPrimaryLanguage()));
     $langs = Internationalization::rfc1766($langs);
diff --git a/include/i18n/en_US/help/tips/settings.system.yaml b/include/i18n/en_US/help/tips/settings.system.yaml
index 28b069775..56e39e2bf 100644
--- a/include/i18n/en_US/help/tips/settings.system.yaml
+++ b/include/i18n/en_US/help/tips/settings.system.yaml
@@ -93,6 +93,20 @@ collision_avoidance:
         <br><br>
         Enter <span class="doc-desc-opt">0</span> to disable the lockout feature.
 
+allow_iframes:
+    title: Allow iFrames
+    content: >
+        Enter comma separated list of domains for the system to be framed
+        in. If left empty, the system will default to 'self'. This accepts
+        domain wildcards, HTTP/HTTPS URL scheme, and port numbers.
+        <br><br>
+        <b>Example:</b>
+        <br>
+        https://domain.tld, sub.domain.tld:443, http://*.domain.tld
+    links:
+      - title: Syntax Information (host-source)
+        href: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors#Sources"
+
 # Date and time options
 date_time_options:
     title: Date &amp; Time Options
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index ebd31bfb0..4de7ccd8f 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -1,6 +1,6 @@
 <?php
 header("Content-Type: text/html; charset=UTF-8");
-header("X-Frame-Options: SAMEORIGIN");
+header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';");
 
 $title = ($ost && ($title=$ost->getPageTitle()))
     ? $title : ('osTicket :: '.__('Staff Control Panel'));
diff --git a/include/staff/login.header.php b/include/staff/login.header.php
index 2a4fcdbb6..e1b1b3a5a 100644
--- a/include/staff/login.header.php
+++ b/include/staff/login.header.php
@@ -1,6 +1,6 @@
 <?php
 defined('OSTSCPINC') or die('Invalid path');
-header("X-Frame-Options: SAMEORIGIN");
+header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';");
 ?>
 <!DOCTYPE html>
 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php
index 22502ed5e..37370df97 100644
--- a/include/staff/settings-system.inc.php
+++ b/include/staff/settings-system.inc.php
@@ -131,6 +131,13 @@ $gmtime = Misc::gmtime();
                 <i class="help-tip icon-question-sign" href="#enable_richtext"></i>
             </td>
         </tr>
+        <tr>
+            <td><?php echo __('Allow iFrames'); ?>:</td>
+            <td><input type="text" size="40" name="allow_iframes" value="<?php echo $config['allow_iframes']; ?>">
+                &nbsp;<font class="error">&nbsp;<?php echo $errors['allow_iframes']; ?></font>
+                <i class="help-tip icon-question-sign" href="#allow_iframes"></i>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Date and Time Options'); ?></b>&nbsp;
diff --git a/setup/inc/header.inc.php b/setup/inc/header.inc.php
index 57ceade2e..0bfc0ec85 100644
--- a/setup/inc/header.inc.php
+++ b/setup/inc/header.inc.php
@@ -1,4 +1,6 @@
-<?php header("X-Frame-Options: SAMEORIGIN"); ?>
+<?php
+header("Content-Security-Policy: frame-ancestors '".$cfg->getAllowIframes()."';");
+?>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
     "http://www.w3.org/TR/html4/loose.dtd">
 <html <?php
-- 
GitLab