Changeset - 8d55669e05c2
[Not reviewed]
default
0 5 0
Nathan Brink (binki) - 13 years ago 2012-11-10 00:51:05
ohnobinki@ohnopublishing.net
Add some hacks to support pure HTTPS for WebAdvisor XSS automatic registration hack.

Instead of using a JSONP callback and PHP sessions to store the
TOKENIDX, the XSS script now just updates the URL parameter for the
WebAdvisor login page so that the TOKENIDX will be transferred to
webadvisor.php by a GET variable right when it is needed instead of
asynchronously.

A separate hack to help support HTTPS is that automatic uploading of
assets to Amazon S3 (which has HTTPS access) allows the XSS script to
be served over HTTPS. This eliminates the browser warnings about
accessing mixed secure/insecure content and, thus, hopefully supports
browsers which automatically block insecure content on secure pages.
5 files changed with 115 insertions and 56 deletions:
0 comments (0 inline, 0 general)
.hgignore
Show inline comments
 
style: regex
 

	
 
# ignore all saved schedules, but keep track of the .keep file.
 
^saved_schedules/[^.]
 
^saved_schedules/\.s3_cache
 
# ignore all of cache except for the .keep file
 
^cache/[^.]
 

	
 
# ignore .htaccess because we only want .htaccess.example in the repo
 
^\.htaccess$
 

	
 
# ignore inc/config.inc because we only want inc/config.inc.example in
 
# the repo
 
^inc/config\.inc$
 

	
 
# ignore common unwanted suffixes
 
(~|\.orig|\.rej)$
 

	
 
# Doxygen-generated stuff
 
^slate_permutate.tagfile
 
^html/
inc/class.page.php
Show inline comments
 
@@ -722,48 +722,128 @@ class page
 
	  $port = NULL;
 
	  if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80)
 
	    {
 
	      if ($_SERVER['SERVER_PORT'] == 443 || !empty($_SERVER['HTTPS']))
 
		$proto .= 's';
 
	      if ($_SERVER['SERVER_PORT'] != 433)
 
		$port = $_SERVER['SERVER_PORT'];
 
	    }
 
	  
 
	  $base_uri = $proto . '://' . $host;
 
	  if ($port !== NULL)
 
	    $base_uri .= ':' . $port;
 
	  list($base_request_uri) = explode('?', $_SERVER['REQUEST_URI'], 2);
 
	  $base_uri .= substr($base_request_uri, 0, strrpos($base_request_uri, '/')) . '/';
 
	}
 

	
 
    if (empty($base_uri) && empty($uri))
 
      return './';
 

	
 
    return $base_uri . $uri;
 
  }
 

	
 
  /**
 
   * \brief
 
   *   Resolve an SSL address for a static asset.
 
   *
 
   * This is pretty much a hack in support of another hack. I need to
 
   * provide some assets over SSL; if the local server doesn’t support
 
   * that properly (such as by not having a properly signed SSL
 
   * certificate), a web-storage backend can be used instead. This can
 
   * only be used with static content.
 
   *
 
   * \param $uri
 
   *   The path to a static file which needs to be served over SSL.
 
   */
 
  public static function uri_resolve_sslasset($uri, $type)
 
  {
 
    global $s3_bucket, $s3_accesskey, $s3_secretkey;
 

	
 
    $testuri = page::uri_resolve($uri);
 
    if (!strncmp($testuri, 'https://', strlen('https://')))
 
      /*
 
       * The user is already accessing this page as SSL, so serving
 
       * another asset over the same channel will not appear any less
 
       * trusted to the user.
 
       */
 
      return $testuri;
 

	
 
    /*
 
     * Use an external service if configured.
 
     */
 
    if (!empty($s3_bucket) && !empty($s3_accesskey) && !empty($s3_secretkey))
 
      {
 
	/*
 
	 * Load S3 cache.
 
	 */
 
	$dirpath = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR;
 
	$s3_cache_path = $dirpath . 'saved_schedules' . DIRECTORY_SEPARATOR . '.s3_cache';
 
	$s3_cache = @unserialize(file_get_contents($s3_cache_path));
 
	if (empty($s3_cache))
 
	  $s3_cache = array();
 

	
 
	$path = $dirpath . $uri;
 
	$sha1 = sha1_file($path);
 

	
 
	if (empty($s3_cache[$sha1]))
 
	  {
 
	    @include 'S3.php';
 
	    if (class_exists('S3'))
 
	      {
 
		$s3 = new S3($s3_accesskey, $s3_secretkey);
 
		$bucket = $s3->getBucket($s3_bucket);
 
		if ($bucket === FALSE)
 
		  $bucket = $s3->putBucket($s3_bucket, S3::ACL_PUBLIC_READ);
 
		if ($bucket !== FALSE)
 
		  if ($s3->putObject(S3::inputFile($path), $s3_bucket, $sha1, S3::ACL_PUBLIC_READ, array(), array('Content-Type' => $type)))
 
		    {
 
		      $s3_cache[$sha1]['uri'] = 'https://' . $s3_bucket . '.s3.amazonaws.com/' . $sha1;
 
		      file_put_contents($s3_cache_path, serialize($s3_cache), LOCK_EX);
 
		    }
 
	      }
 
	  }
 
	if (!empty($s3_cache[$sha1]['uri']))
 
	  return $s3_cache[$sha1]['uri'];
 
      }
 

	
 
    if (!strncmp($testuri, 'http://', strlen('http://')))
 
      {
 
	/* Test if we can create a local HTTPS connection… */
 
	$curl = curl_init();
 
	curl_setopt($curl, CURLOPT_USERAGENT, SP_PACKAGE_NAME . '/' . SP_PACKAGE_VERSION);
 
	$testuri2 = 'https' . substr($testuri, strlen('https'));
 
	curl_setopt($curl, CURLOPT_URL, $testuri2);
 
	curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
 
	$result = curl_exec($curl);
 
	curl_close($curl);
 
	if (!empty($result) && sha1($result) === $sha1)
 
	  return $testuri2;
 
      }
 
    return $testuri;
 
  }
 

	
 
  /**
 
   * \brief
 
   *   Form a query string from a map.
 
   *
 
   * \param $query
 
   *   The map of keys onto values to form into a querystring.
 
   * \param $question
 
   *   Include the question mark which delimits the querystring in a
 
   *   URI.
 
   * \return
 
   *   A querystring suitable for appending to a URI. Includes the `?'
 
   *   by default.
 
   */
 
  public static function query_string(array $query, $question = TRUE)
 
  {
 
    $query_string_parts = array();
 
    foreach ($query as $param => $values)
 
      {
 
	if (!is_array($values))
 
	  $values = array($values);
 
	foreach ($values as $value)
 
	  $query_string_parts[] = rawurlencode($param) . '=' . rawurlencode($value);
 
      }
 
    if (count($query_string_parts))
 
      return ($question ? '?' : '') . implode('&', $query_string_parts);
 
    return '';
inc/config.inc.example
Show inline comments
 
@@ -148,24 +148,40 @@
 
/**
 
 * \brief
 
 *   Specify whether or not to log feedback to disk
 
 *
 
 * Set to TRUE to log to file in addition to email logging (if available).
 
 * $feedback_disk_log_file must be set to a fully-qualified filepath.
 
 * 
 
 */
 
/* $feedback_disk_log = TRUE; */
 
/* $feedback_disk_log_file = "/var/log/sp-feedback.log"; */
 

	
 

	
 
/**
 
 * \brief
 
 *   A banner to display to users on input.php for temporary warnings.
 
 *
 
 * Used to inform users about known data problems, etc.
 
 *
 
 * Set to a string to display a banner. Otherwise, set to FALSE to
 
 * hide the banner. Must be a valid XHTML fragment which may be placed
 
 * into a <div /> verbatim.
 
 */
 
/* $input_warning_banner = FALSE; */
 
/* $input_warning_banner = '<p>Warning: BIOL-111\'s autocomplete data does not include all sections. Please use <a href="http://csx.calvin.edu/sp/input.php?s=7578">schedule 7578</a> to get a correct BIOL-111 entry.</p>'; */
 

	
 
/**
 
 * \brief
 
 *   Amazon S3 credentials for best-effort SSL hack.
 
 *
 
 * Setting S3 credentials will enable slate_permutate to serve certain
 
 * content over an HTTPS connection for the (old) WebAdvisor XSS for
 
 * automatic registration. You must specify a bucket name which is
 
 * either nonexistent (available) or already owned by your S3 account.
 
 *
 
 * You must have installed the amazon-s3-php-class PHP library to take
 
 * advantage of this feature.
 
 */
 
/* $s3_accesskey = ''; */
 
/* $s3_secretkey = ''; */
 
/* $s3_bucket = 'myslatepermutate'; */
scripts/webadvisor_tokenidx.js
Show inline comments
 
@@ -35,67 +35,64 @@ var slate_permutate_input_login;
 
				 * case where we're still trying to load the TOKENIDX or
 
				 * something else.
 
				 */
 
				var inputs = document.getElementsByTagName('input');
 
				for (var i = 0; i < inputs.length; i ++)
 
				{
 
						slate_permutate_input_login = inputs.item(i);
 
						if (slate_permutate_input_login.getAttribute('name') == 'SUBMIT2')
 
								break;
 
				}
 
				slate_permutate_input_login.setAttribute('value', 'Discovering TOKENIDX...');
 
				slate_permutate_input_login.setAttribute('disabled', 'disabled');
 

	
 
				/*
 
				 * Discover the TOKENIDX if it's available.
 
				 */
 
				var sp_err = document.getElementById('sp_err');
 
				if (containsParameter(g_tokenIdx))
 
				{
 
					/* Remove the warning about the script not having loaded */
 
					sp_err.replaceChild(document.createTextNode("Slate Permutate TOKENIDX-acquiring script loaded…"), sp_err.firstChild);
 
					sp_err.setAttribute('style', 'color: grey;');
 

	
 
					/* Inform home base of the newly generated TOKENIDX. */
 
						var TOKENIDX = getURLParameter(g_tokenIdx);
 
						var myscript = document.createElement('script');
 
						myscript.setAttribute('type', 'text/javascript');
 
						myscript.setAttribute('src', decodeURIComponent(getURLParameter('SP_CALLBACK')) + 'callback=slate_permutate_token_callback&TOKENIDX=' + TOKENIDX);
 
						document.getElementsByTagName('head').item(0).appendChild(myscript);
 
					var TOKENIDX = getURLParameter(g_tokenIdx);
 
					if (getURLParameter('URL').indexOf('TOKENIDX%3d' + TOKENIDX) === -1)
 
					{
 
						/* %26 = &, setURLParameter doesn’t handle escaping */
 
						setURLParameter('URL', getURLParameter('URL') + '%26TOKENIDX%3d' + TOKENIDX);
 
						window.location.href = getBaseURI(window.location.href) + '?' + getURLParameters();
 
					}
 
					else
 
					{
 
						/* Report to the user that they’ve been fixed up */
 
						slate_permutate_input_login.setAttribute('value', 'LOG IN');
 
						slate_permutate_input_login.removeAttribute('disabled');
 
						sp_err.replaceChild(document.createTextNode('Slate Permutate has acquired WebAdvisor TOKENIDX, ready for login.'), sp_err.firstChild);
 
						sp_err.setAttribute('style', 'color: green;');
 
					}
 
				}
 
				else
 
				{
 
					sp_err.replaceChild(document.createTextNode('Slate Permutate unable to acquire TOKENIDX. You must register manually.'), sp_err.firstChild);
 
					sp_err.setAttribute('style', 'color: red; background: yellow;');
 
						alert('Unable to discover WebAdvisor TOKENIDX. You must register manually.');
 
				}
 
		}
 

	
 
		/*
 
		 * Register to run after either of getWindowHTML(),
 
		 * setWindowHTML(), or displayFormHTML() have been run. These are
 
		 * run after onload="", so they are required if we're to wait for
 
		 * the DOM to load...
 
		 */
 
		var funcs = ['getWindowHTML', 'setWindowHTML', 'displayFormHTML'];
 
		for (var i = 0; i < funcs.length; i ++)
 
		{
 
				var func = window[funcs[i]];
 
				window[funcs[i]] = function() {
 
						func();
 
						slate_permutate_onload();
 
				};
 
		}
 
})();
 

	
 
function slate_permutate_token_callback(result)
 
{
 
		if (result)
 
		{
 
				slate_permutate_input_login.setAttribute('value', 'LOG IN');
 
				slate_permutate_input_login.removeAttribute('disabled');
 

	
 
			var sp_err = document.getElementById('sp_err');
 
			sp_err.replaceChild(document.createTextNode('Slate Permutate has acquired WebAdvisor TOKENIDX, ready for login.'), sp_err.firstChild);
 
			sp_err.setAttribute('style', 'color: green;');
 
		}
 
}
webadvisor.php
Show inline comments
 
<?php /* -*- mode: php; -*- */
 
/*
 
 * Copyright 2012 Nathan Phillip Brink
 
 *
 
 * This file is part of SlatePermutate.
 
 *
 
 * SlatePermutate is free software: you can redistribute it and/or modify
 
 * it under the terms of the GNU Affero General Public License as published by
 
 * the Free Software Foundation, either version 3 of the License, or
 
 * (at your option) any later version.
 
 *
 
 * SlatePermutate is distributed in the hope that it will be useful,
 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
 * GNU Affero General Public License for more details.
 
 *
 
 * You should have received a copy of the GNU Affero General Public License
 
 * along with SlatePermutate.  If not, see <http://www.gnu.org/licenses/>.
 
 */
 

	
 
require_once('inc/class.page.php');
 

	
 
/*
 
 * Handle the scripts/webadvisor_tokenidx.js making a TOKENIDX
 
 * callback, storing that TOKENIDX in our SESSION for later use.
 
 */
 
if (!empty($_GET['TOKENIDX']))
 
  {
 
    page::session_start();
 

	
 
    $_SESSION['webadvisor_TOKENIDX'] = $_GET['TOKENIDX'];
 

	
 
    $result = 'received ' . $_GET['TOKENIDX'];
 

	
 
    header('Content-Type: text/javascript; charset=utf-8');
 

	
 
    if ($jsonp = !empty($_GET['callback']))
 
      echo $_GET['callback'] . '(';
 
    echo json_encode($result);
 
    if ($jsonp)
 
      echo ");\n";
 
    if ($jsonp && !empty($_GET['destination']))
 
      echo 'document.location.href = ' . json_encode($_GET['destination']) . ";\n";
 
    exit;
 
  }
 

	
 
$page = page::page_create('WebAdvisor');
 
$school = $page->get_school();
 

	
 
if (empty($school['webadvisor_url']))
 
  {
 
    if (!empty($school['registration_url']) && preg_match(',(.*/WebAdvisor),', $school['registration_url'], $matches))
 
      $school['webadvisor_url'] = $matches[1];
 
    else
 
      $school['webadvisor_url'] = $school['url'] . 'WebAdvisor';
 
  }
 

	
 
/**
 
 * \brief
 
 *   Calculate the URI necessary for logging into WebAdvisor.
 
 *
 
 * \param $school
 
 *   The school.
 
 * \param $dest
 
 *   The URI to visit after the user has logged into WebAdvisor and
 
 *   the TOKENIDX has been communicated to $tokenidx_callback.
 
 * \param $tokenidx_callback
 
 *   A JSONP-compatible callback which must be passed the TOKENIDX
 
 *   parameter the WebAdvisor is using. Treat as if is terminated with
 
 *   a `?' -- i.e., just append the querystring without the `?' to
 
@@ -100,91 +76,80 @@ function webadvisor_login($page, array $
 
   * will both initialize the user's browser with a token cookie and
 
   * then redirect to URL. Trying to use the proper way of loading the
 
   * LGRQ (using TYPE=P&PID=UT-LGRQ&PROCESS=-XUTAUTH01) doesn't work
 
   * because it drops and ignores our URL parameter, leaving the user
 
   * at the KV site. No other URL I've fiddled with seems to be able
 
   * to do this combination of logging in and returning the user to us
 
   * or a URI of our choosing. Once the user's browser has been
 
   * initialized with a TOKENIDX, loading the page
 
   * SS=LGRQ&URL=<URI>&ERROR=<XSS> will preserve the ERROR=<XSS>
 
   * necessary for our XSS and insert it into the login page.
 
   *
 
   * HOWEVER, if the browser already has a TOKENIDX-related cookie,
 
   * then visiting TOKENIDX=&SS=LGRQ&URL=<URL> will cause WebAdvisor
 
   * to keep redirecting to itself infinitely. Similarly, if the
 
   * browser does not yet have a TOKENIDX-related cookie,
 
   * SS=LGRQ&URL=<URL> will redirect the user to URL without giving
 
   * the user a cookie. Thus, our strategy is to specify
 
   * LASTTOKEN=NULL: this is the magic which prevents the silly
 
   * infinite looping when the user already has cookies and also works
 
   * when the user doesn’t have a cookie to start with.
 
   */
 

	
 
  $login_form_uri = $school['webadvisor_url'] . '?LASTTOKEN=NULL&SS=LGRQ&URL=' . rawurlencode($dest)
 
    . '&SP_CALLBACK=' . rawurlencode($tokenidx_callback)
 
    . '&ERROR=' . rawurlencode('<script type="text/javascript" src="' . htmlentities(page::uri_resolve('scripts/webadvisor_tokenidx.js?20121110f'), ENT_QUOTES) . '"></script><span id="sp_err">Slate Permutate loading… (automatic registration may not be working)</span>');
 
    . '&ERROR=' . rawurlencode('<script type="text/javascript" src="' . htmlentities(page::uri_resolve_sslasset('scripts/webadvisor_tokenidx.js', 'text/javascript'), ENT_QUOTES) . '"></script><span id="sp_err">Slate Permutate loading… (automatic registration may not be working)</span>');
 
  redir($login_form_uri);
 
}
 

	
 
function redir($dest)
 
{
 
  header('HTTP/1.1 302 Found');
 
  header('Location: ' . $dest);
 
  header('Content-Type: text/plain; charset=utf-8');
 
  echo 'Location: ' . $dest;
 
  exit;
 
}
 

	
 
/*
 
 * If the page load was not a redirection from webadvisor, we must
 
 * clear our local cache of TOKENIDX's value. We need to get a new
 
 * token because we can't guess what SS= value the ST-WERG form will
 
 * take unless if we start with a new TOKENIDX which doesn't have any
 
 * SSes yet. Also, the old token may have (very likely) expired
 
 * because of the short login timeout.
 
 */
 
if (!isset($_GET['from_webadvisor']))
 
  unset($_SESSION['webadvisor_TOKENIDX']);
 

	
 
if (empty($_SESSION['webadvisor_TOKENIDX']))
 
if (empty($_GET['TOKENIDX']))
 
  {
 
    /*
 
     * Get a token for the ST-WERG form and have the user perform the
 
     * WebAdvisor-specific login. This can only be done after the
 
     * login form has an SS allocated for it.
 
     */
 
    webadvisor_login($page, $school, page::uri_resolve('webadvisor.php') . '?r=' . rand()
 
		     . '&sections=' . rawurlencode(empty($_GET['sections']) ? '' : $_GET['sections'])
 
		     . '&school=' . rawurlencode($school['id']),
 
		     page::uri_resolve('webadvisor.php?'));
 
  }
 

	
 
/*
 
 * Use the hopefully-still-valid TOKENIDX to initialize an ST-WERG
 
 * (STudent Web[A]dvisor Express ReGistration) form. When that form is
 
 * iniailized, assume that it has SS=1 and submit the form. &APP=ST
 
 */
 
$TOKENIDX = $_SESSION['webadvisor_TOKENIDX'];
 
$TOKENIDX = $_GET['TOKENIDX'];
 
$page->head();
 
echo '<form id="sp-webadvisor-form" action="' . htmlentities($school['webadvisor_url'] . '?TOKENIDX=' . $TOKENIDX . '&SS=1', ENT_QUOTES) . '" method="post">' . PHP_EOL;
 
echo '<p>';
 

	
 
$uri = $school['webadvisor_url'] . '?TOKENIDX=' . $TOKENIDX . '&TYPE=P&PID=ST-WERG';
 
$onload_html = '="' . htmlentities('javascript:document.getElementById(\'sp-webadvisor-form\').submit()', ENT_QUOTES) . '"';
 
echo '  <img src="' . htmlentities($uri, ENT_QUOTES) . '" alt="Loading WebAdvisor Express Registration form (ST-WERG)…"'
 
. ' onload' . $onload_html . ' onerror' . $onload_html . ' />' . PHP_EOL;
 
echo '  If you are not redirected after 16 seconds, you may try: ' . PHP_EOL;
 

	
 
$sections = explode(',', empty($_GET['sections']) ? '' : $_GET['sections']);
 
echo '  <input type="hidden" name="LIST.VAR1_CONTROLLER" value="LIST.VAR1" />' . PHP_EOL;
 
echo '  <input type="hidden" name="LIST.VAR1_MEMBERS" value="LIST.VAR1*LIST.VAR2*LIST.VAR3*LIST.VAR4*LIST.VAR5" />' . PHP_EOL;
 
for ($i = 1; $i <= 5; $i ++)
 
  echo //'  <input type="hidden" name="LIST.VAR' . $i . '_MAX" value="' . count($sections) . '" />' . PHP_EOL;
 
    '  <input type="hidden" name="LIST.VAR' . $i . '_MAX" value="10" />' . PHP_EOL;
 
$course_num = 1;
 
foreach ($sections as $course)
 
  {
 
    echo '  <input type="hidden" name="LIST.VAR1_' . $course_num . '" value="' . htmlentities($course, ENT_QUOTES) . '" />' . PHP_EOL;
 
    for ($i = 2; $i <= 5; $i ++)
 
      echo '  <input type="hidden" name="LIST.VAR' . $i . '_' . $course_num . '" value="" />' . PHP_EOL;
 
    $course_num ++;
 
  }
0 comments (0 inline, 0 general)