From b2b9d62990f868e1e390ec6009ab6e4b5dc9574f Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Fri, 1 Aug 2008 08:45:20 -0600 Subject: [PATCH] Implement dynamic form rewriting; check headers for text/html before rewriting. Signed-off-by: Edward Z. Yang --- README.txt | 12 ------------ csrf-magic.js | 21 +++++++++++++++++++-- csrf-magic.php | 37 ++++++++++++++++++++++++++++++++++--- test.php | 10 ++++++++-- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/README.txt b/README.txt index 8995170..201ba6b 100644 --- a/README.txt +++ b/README.txt @@ -153,15 +153,3 @@ esp the frame breaker which we can automatically write in. When an AJAX call is performed within an iframe, our rewriting of XMLHttpRequest my fail for the first page load. Subsequent page loads will work properly. - - -7. TODO - - * Minify csrf-magic.js for performance. - - * Auto-generate secret. - - * (?) Make "first time" session more robust by double-submitting. - - * Account for JavaScript generated-forms with some JavaScript that loads into - some global onsubmit handler and checks form submissions accordingly. diff --git a/csrf-magic.js b/csrf-magic.js index e898518..121f31e 100644 --- a/csrf-magic.js +++ b/csrf-magic.js @@ -4,6 +4,7 @@ * Rewrites XMLHttpRequest to automatically send CSRF token with it. In theory * plays nice with other JavaScript libraries, needs testing though. */ + // Here are the basic overloaded method definitions // The wrapper must be set BEFORE onreadystatechange is written to, since // a bug in ActiveXObject prevents us from properly testing for it. @@ -74,7 +75,6 @@ CsrfMagic.prototype = { } // , } - // proprietary CsrfMagic.prototype._updateProps = function() { this.readyState = this.csrf.readyState; @@ -90,6 +90,20 @@ CsrfMagic.process = function(base) { if (base) return prepend + '&' + base; return prepend; } +// callback function for when everything on the page has loaded +CsrfMagic.end = function() { + forms = document.getElementsByTagName('form'); + for (var i = 0; i < forms.length; i++) { + form = forms[i]; + if (form.method.toUpperCase() !== 'POST') continue; + if (form.elements[csrfMagicName]) continue; + var input = document.createElement('input'); + input.setAttribute('name', csrfMagicName); + input.setAttribute('value', csrfMagicToken); + input.setAttribute('type', 'hidden'); + form.appendChild(input); + } +} // Sets things up for Mozilla/Opera/nice browsers if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) { @@ -108,7 +122,7 @@ if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) { x.setRequestHeader = c.setRequestHeader; } else { // The only way we can do this is by modifying a library you have been - // using. We plan to support YUI, script.aculo.us, prototype, MooTools, + // using. We support YUI, script.aculo.us, prototype, MooTools, // jQuery, Ext and Dojo. if (window.jQuery) { // jQuery didn't implement a new XMLHttpRequest function, so we have @@ -159,3 +173,6 @@ if (window.XMLHttpRequest && window.XMLHttpRequest.prototype) { } } } + +// Form rewriter + diff --git a/csrf-magic.php b/csrf-magic.php index 0205e4c..59ada1d 100644 --- a/csrf-magic.php +++ b/csrf-magic.php @@ -48,6 +48,13 @@ $GLOBALS['csrf']['rewrite-js'] = false; $GLOBALS['csrf']['secret'] = ''; /** + * Set this to false to disable csrf-magic's output handler, and therefore, + * its rewriting capabilities. If you're serving non HTML content, you should + * definitely set this false. + */ +$GLOBALS['csrf']['rewrite'] = true; + +/** * Whether or not to use IP addresses when binding a user to a token. This is * less reliable and less secure than sessions, but is useful when you need * to give facilities to anonymous users and do not wish to maintain a database @@ -105,6 +112,25 @@ $GLOBALS['csrf']['xhtml'] = true; * inject our JavaScript library. */ function csrf_ob_handler($buffer, $flags) { + // Extra PHP5 sugar; if headers_list() is available we can check if the + // user set a different content-type + if (function_exists('headers_list')) { + $headers = headers_list(); + $is_html = true; + foreach ($headers as $header) { + if (strpos($header, ':') === false) continue; + list($k, $v) = explode(':', $header); + if ( + strcasecmp(trim($k), 'Content-Type') === 0 && + // only compare the first nine, since charset could be set + strncasecmp(trim($v), 'text/html', 9) !== 0 + ) { + $is_html = false; + break; + } + } + if (!$is_html) return $buffer; + } $token = csrf_get_token(); $name = $GLOBALS['csrf']['input-name']; $endslash = $GLOBALS['csrf']['xhtml'] ? ' /' : ''; @@ -119,6 +145,11 @@ function csrf_ob_handler($buffer, $flags) { '$1', $buffer ); + $script = ''; + $buffer = str_ireplace('', $script . '', $buffer, $count); + if (!$count) { + $buffer .= $script; + } } return $buffer; } @@ -246,9 +277,9 @@ function csrf_get_secret() { return ''; } -// Initialize our handler -ob_start('csrf_ob_handler'); // Load user configuration if (function_exists('csrf_startup')) csrf_startup(); +// Initialize our handler +if ($GLOBALS['csrf']['rewrite']) ob_start('csrf_ob_handler'); // Perform check -if (!$GLOBALS['csrf']['defer']) csrf_check(); +if (!$GLOBALS['csrf']['defer']) csrf_check(); diff --git a/test.php b/test.php index ca37d41..35f1a4f 100644 --- a/test.php +++ b/test.php @@ -2,6 +2,7 @@ function csrf_startup() { csrf_conf('rewrite-js', 'csrf-magic.js'); + if (isset($_POST['ajax'])) csrf_conf('rewrite', false); } include dirname(__FILE__) . '/csrf-magic.php'; @@ -49,7 +50,12 @@ if (isset($_POST['ajax'])) {

How about some JavaScript?

- + +