diff --git a/NEWS.md b/NEWS.md new file mode 100644 index 0000000..c0e360a --- /dev/null +++ b/NEWS.md @@ -0,0 +1,126 @@ +# News + +## 1.1.0 released 2020-02-23 + +Bug Fixes + +- In some server environments, being behind a load balancer and enabling IP + restrictions would be ineffective as other users would all appear to be from + the same remote address + +Features + +- Allow logging of the CSRF process which is useful for third party develoeprs + when they are trying to incorporate the library to see what steps are being + taken and from where. + + Configuration var: `log_file` + +- Allow logging to also be echoed to console + + Configuration var: `log_echo` + +- Allow specifying the location of the CSRF secret file as some package + maintainers may prefer to relocate the secret file to a hidden location that + is readable only when installing the package and not be the application. + + Configuration var: `path_secret` + +- Allow specifying the startup function as some callers may wish to keep in line + with their own code formats. + + Configuration var `startup_func` + +- Allow configuration of which hashing function to use. It is expected that the + user configuring this option will known what hash functions are availble or it + could cause runtime errors. + + Configuration var `hash` + +- Allow use of session_id() to be configured via configuration variable so that + other mechanisms can be utilsed instead. + + Configuration var `session` + +## 1.0.5 released 2014-07-24 + +Bug Fixes + +- In some server environments, IP address was not being detected properly. + Thanks Bianka Martinovic for reporting. + +Security Fixes + +- Hashing now uses an HMAC to prevent length extension attacks. + +Features + +- New option 'disable' which allows you to conditionally disable the CSRF + protection. Requested by Justin Carlson. + +## 1.0.4 released 2013-07-17 + +Security Fixes + +- When secret key was not explicitly set, it was not being used by the + `csrf_hash()` function. Thanks sparticvs for reporting. + +Features + +- The default 'CSRF check failed' page now offers a handy 'Try again' button, + which resubmits the form. + +Bug Fixes + +- The fix for 1.0.3 inadvertantly turned off XMLHttpRequest + overloading for all browsers; it has now been fixed to only + apply to IE. + +## 1.0.3 released 2012-01-31 + +Bug Fixes + +- Internet Explorer 8 adds support for XMLHttpRequest.prototype, + but this support is broken for method overloading. We + explicitly disable JavaScript overloading for Internet Explorer. + Thanks Kelly Lu for reporting. + +- A global declaration was omitted, resulting in a variable + not being properly introduced in PHP 5.3. Thanks Whitney Beck for + reporting. + +## 1.0.2 released 2009-03-08 + +Security Fixes + +- Due to a typo, csrf-magic accidentally treated the secret key + as always present. This means that there was a possible CSRF + attack against users without any cookies. No attacks in the + wild were known at the time of this release. Thanks Jakub + Vrána for reporting. + +## 1.0.1 released 2008-11-02 + +### New Features + +- Support for composite tokens; this also fixes a bug with using + IP-based tokens for users with cookies disabled. + +- Native support cookie tokens; use csrf_conf('cookie', $name) to + specify the name of a cookie that the CSRF token should be + placed in. This is useful if you have a Squid cache, and need + to configure it to ignore this token. + +- Tips/tricks section in README.txt. + +- There is now a two hour expiration time on all tokens. This + can be modified using csrf_conf('expires', $seconds). + +- ClickJacking protection using an iframe breaker. Disable with + csrf_conf('frame-breaker', false). + +Bug Fixes + +- CsrfMagic.send() incorrectly submitted GET requests twice, + once without the magic token and once with the token. Reported + by Kelly Lu . diff --git a/NEWS.txt b/NEWS.txt deleted file mode 100644 index 66d52f6..0000000 --- a/NEWS.txt +++ /dev/null @@ -1,69 +0,0 @@ - - [[ news ]] - -1.0.4 released 2013-07-17 - - [SECURITY FIXES] - - - When secret key was not explicitly set, it was not being used - by the csrf_hash() function. Thanks sparticvs for reporting. - - [FEATURES] - - - The default 'CSRF check failed' page now offers a handy 'Try - again' button, which resubmits the form. - - [BUG FIXES] - - - The fix for 1.0.3 inadvertantly turned off XMLHttpRequest - overloading for all browsers; it has now been fixed to only - apply to IE. - -1.0.3 released 2012-01-31 - - [BUG FIXES] - - - Internet Explorer 8 adds support for XMLHttpRequest.prototype, - but this support is broken for method overloading. We - explicitly disable JavaScript overloading for Internet Explorer. - Thanks Kelly Lu for reporting. - - - A global declaration was omitted, resulting in a variable - not being properly introduced in PHP 5.3. Thanks Whitney Beck for - reporting. - -1.0.2 released 2009-03-08 - - [SECURITY FIXES] - - - Due to a typo, csrf-magic accidentally treated the secret key - as always present. This means that there was a possible CSRF - attack against users without any cookies. No attacks in the - wild were known at the time of this release. Thanks Jakub - Vrána for reporting. - -1.0.1 released 2008-11-02 - - [NEW FEATURES] - - - Support for composite tokens; this also fixes a bug with using - IP-based tokens for users with cookies disabled. - - - Native support cookie tokens; use csrf_conf('cookie', $name) to - specify the name of a cookie that the CSRF token should be - placed in. This is useful if you have a Squid cache, and need - to configure it to ignore this token. - - - Tips/tricks section in README.txt. - - - There is now a two hour expiration time on all tokens. This - can be modified using csrf_conf('expires', $seconds). - - - ClickJacking protection using an iframe breaker. Disable with - csrf_conf('frame-breaker', false). - - [BUG FIXES] - - - CsrfMagic.send() incorrectly submitted GET requests twice, - once without the magic token and once with the token. Reported - by Kelly Lu . diff --git a/README.md b/README.md new file mode 100644 index 0000000..9915cf9 --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# CSRF Magic + +Add the following line to the top of all web-accessible PHP pages. If you have a +common file included by everything, put it there. + +```php +include_once '/path/to/csrf-magic.php'; +``` + +Do it, test it, then forget about it. csrf-magic is protecting you if nothing +bad happens. Read on if you run into problems. + +## TIPS AND TRICKS + +* If your JavaScript and AJAX is persistently getting errors, check the AJAX + section below on how to fix. + +* The CSS overlay protection makes it impossible to display your website in + frame/iframe elements. You can disable it with csrf_conf('frame-breaker', + false) in your csrf_startup() function. + +* csrf-magic will start a session. To disable, use csrf_conf('auto-session', + false) in your csrf_startup() function. + +* The default error message is a little user unfriendly. Write your own + function which outputs an error message and set csrf_conf('callback', + 'myCallbackFunction') in your csrf_startup() function. + +* Make sure csrf_conf('secret', 'ABCDEFG') has something random in it. If the + directory csrf-magic.php is in is writable, csrf-magic will generate a secret + key for you in the csrf-secret.php file. + +* Remember you can use auto_prepend to include csrf-magic.php on all your pages. + You may want to create a stub file which you can include that includes + csrf-magic.php as well as performs configuration. + +* The default expiration time for tokens is two hours. If you expect your users + to need longer to fill out forms, be sure to enable double submission when the + token is invalid. + +## AJAX + +csrf-magic has the ability to dynamically rewrite AJAX requests which use +XMLHttpRequest. However, due to the invasiveness of this procedure, it is not +enabled by default. You can enable it by adding this code before you include +csrf-magic.php. + +```php +function csrf_startup() { + csrf_conf('rewrite-js', '/web/path/to/csrf-magic.js'); +} +// include_once '/path/to/csrf-magic.php'; +``` + +**NOTE:** *Be sure to place csrf-magic.js somewhere web accessible.* + +The default method CSRF Magic uses to rewrite AJAX requests will only work for +browsers with support for XmlHttpRequest.prototype (this excludes all versions +of Internet Explorer). See this page for more information: +[Prototypes and xmlhtprequest](http://stackoverflow.com/questions/664315/internet-explorer-8-prototypes-and-xmlhttprequest) + +However, csrf-magic.js will automatically detect and play nice with the +following JavaScript frameworks: + +* jQuery + +* Prototype + +* MooTools + +* Ext + +* Dojo + +**Note:** As of 2013-07-16, it has been a long time since this manual support +has been updated, and some JavaScript libraries have placed their copies of XHR +in local variables in closures, which makes it difficult for us to monkey-patch +it in automatically.) + +To rewrite your own JavaScript library to use csrf-magic.js, you should modify +your function that generates XMLHttpRequest to have this at the end: + +```php +return new CsrfMagic(xhrObject); +``` + +With whatever xhrObject may be. If you have literal instances of XMLHttpRequest +in your code, find and replace `new XMLHttpRequest` with `new CsrfMagic` +(CsrfMagic will automatically instantiate an XMLHttpRequest object in a +cross-platform manner as necessary). + +If you don't want csrf-magic monkeying around with your XMLHttpRequest object, +you can manually rewrite your AJAX code to include the variable. The important +information is stored in the global variables csrfMagicName and csrfMagicToken. +CsrfMagic.process may also be of interest, as it takes one parameter, a +querystring, and prepends the CSRF token to the value. + +## BASIC CONFIGURATION + +csrf-magic has some configuration options that you can set inside the +`csrf_startup()` function. They are described in csrf-magic.php, and you can set +them using the convenience `function csrf_conf($name, $value)`. + +For example, this is a recommended configuration suggested by the upstream +providers. It should be noted that Cacti has a custom implementation of this +which is included with this repository. + +```php +/** + * This is a function that gets called if a csrf check fails. csrf-magic will + * then exit afterwards. + */ +function my_csrf_callback() { + echo "You're doing bad things young man!"; +} + +function csrf_startup() { + + // While csrf-magic has a handy little heuristic for determining whether + // or not the content in the buffer is HTML or not, you should really + // give it a nudge and turn rewriting *off* when the content is + // not HTML. Implementation details will vary. + if (isset($_POST['ajax'])) csrf_conf('rewrite', false); + + // This is a secret value that must be set in order to enable username + // and IP based checks. Don't show this to anyone. A secret id will + // automatically be generated for you if the directory csrf-magic.php + // is placed in is writable. + csrf_conf('secret', 'ABCDEFG123456'); + + // This enables JavaScript rewriting and will ensure your AJAX calls + // don't stop working. + csrf_conf('rewrite-js', '/csrf-magic.js'); + + // This makes csrf-magic call my_csrf_callback() before exiting when + // there is a bad csrf token. This lets me customize the error page. + csrf_conf('callback', 'my_csrf_callback'); + + // While this is enabled by default to boost backwards compatibility, + // for security purposes it should ideally be off. Some users can be + // NATted or have dialup addresses which rotate frequently. Cookies + // are much more reliable. + csrf_conf('allow-ip', false); + +} + +// Finally, include the library +include_once '/path/to/csrf-magic.php'; +``` + +The configuration for CSRF is stored in the PHP global array GLOBALS['csrf'] +which is applied from csrf-conf.php in the same directory as the csrf-magic.php +and should be read to see what additional settings are available. + +## THANKS + +My thanks to Chris Shiflett, for unintentionally inspiring the idea, as well as +telling me the original variant of the Bob and Mallory story, and the Django +CSRF Middleware authors, who thought up of this before me. Gareth Heyes +suggested using the frame-breaker option to protect against CSS overlay attacks. diff --git a/README.txt b/README.txt deleted file mode 100644 index 98d225d..0000000 --- a/README.txt +++ /dev/null @@ -1,160 +0,0 @@ - - [[ csrf-magic ]] - -Add the following line to the top of all web-accessible PHP pages. If you have -a common file included by everything, put it there. - - include_once '/path/to/csrf-magic.php'; - -Do it, test it, then forget about it. csrf-magic is protecting you if nothing -bad happens. Read on if you run into problems. - - - TABLE OF CONTENTS - + ------------------- + - 1. TIPS AND TRICKS - 2. AJAX - 3. CONFIGURE - 4. THANKS - 5. FOOTNOTES - + ------------------- + - - -1. TIPS AND TRICKS - - * If your JavaScript and AJAX is persistently getting errors, check the - AJAX section below on how to fix. - - * The CSS overlay protection makes it impossible to display your website - in frame/iframe elements. You can disable it with - csrf_conf('frame-breaker', false) in your csrf_startup() function. - - * csrf-magic will start a session. To disable, use csrf_conf('auto-session', - false) in your csrf_startup() function. - - * The default error message is a little user unfriendly. Write your own - function which outputs an error message and set csrf_conf('callback', - 'myCallbackFunction') in your csrf_startup() function. - - * Make sure csrf_conf('secret', 'ABCDEFG') has something random in it. If - the directory csrf-magic.php is in is writable, csrf-magic will generate - a secret key for you in the csrf-secret.php file. - - * Remember you can use auto_prepend to include csrf-magic.php on all your - pages. You may want to create a stub file which you can include that - includes csrf-magic.php as well as performs configuration. - - * The default expiration time for tokens is two hours. If you expect your - users to need longer to fill out forms, be sure to enable double - submission when the token is invalid. - - -2. AJAX - -csrf-magic has the ability to dynamically rewrite AJAX requests which use -XMLHttpRequest. However, due to the invasiveness of this procedure, it is -not enabled by default. You can enable it by adding this code before you -include csrf-magic.php. - - function csrf_startup() { - csrf_conf('rewrite-js', '/web/path/to/csrf-magic.js'); - } - // include_once '/path/to/csrf-magic.php'; - -(Be sure to place csrf-magic.js somewhere web accessible). - -The default method CSRF Magic uses to rewrite AJAX requests will -only work for browsers with support for XmlHttpRequest.prototype (this excludes -all versions of Internet Explorer). See this page for more information: -http://stackoverflow.com/questions/664315/internet-explorer-8-prototypes-and-xmlhttprequest - -However, csrf-magic.js will -automatically detect and play nice with the following JavaScript frameworks: - - * jQuery - * Prototype - * MooTools - * Ext - * Dojo - -(Note 2013-07-16: It has been a long time since this manual support has -been updated, and some JavaScript libraries have placed their copies of XHR -in local variables in closures, which makes it difficult for us to monkey-patch -it in automatically.) - -To rewrite your own JavaScript library to use csrf-magic.js, you should modify -your function that generates XMLHttpRequest to have this at the end: - - return new CsrfMagic(xhrObject); - -With whatever xhrObject may be. If you have literal instances of XMLHttpRequest -in your code, find and replace ''new XMLHttpRequest'' with ''new CsrfMagic'' -(CsrfMagic will automatically instantiate an XMLHttpRequest object in a -cross-platform manner as necessary). - -If you don't want csrf-magic monkeying around with your XMLHttpRequest object, -you can manually rewrite your AJAX code to include the variable. The important -information is stored in the global variables csrfMagicName and csrfMagicToken. -CsrfMagic.process may also be of interest, as it takes one parameter, a -querystring, and prepends the CSRF token to the value. - - -3. CONFIGURE - -csrf-magic has some configuration options that you can set inside the -csrf_startup() function. They are described in csrf-magic.php, and you can -set them using the convenience function csrf_conf($name, $value). - -For example, this is a recommended configuration: - - /** - * This is a function that gets called if a csrf check fails. csrf-magic will - * then exit afterwards. - */ - function my_csrf_callback() { - echo "You're doing bad things young man!"; - } - - function csrf_startup() { - - // While csrf-magic has a handy little heuristic for determining whether - // or not the content in the buffer is HTML or not, you should really - // give it a nudge and turn rewriting *off* when the content is - // not HTML. Implementation details will vary. - if (isset($_POST['ajax'])) csrf_conf('rewrite', false); - - // This is a secret value that must be set in order to enable username - // and IP based checks. Don't show this to anyone. A secret id will - // automatically be generated for you if the directory csrf-magic.php - // is placed in is writable. - csrf_conf('secret', 'ABCDEFG123456'); - - // This enables JavaScript rewriting and will ensure your AJAX calls - // don't stop working. - csrf_conf('rewrite-js', '/csrf-magic.js'); - - // This makes csrf-magic call my_csrf_callback() before exiting when - // there is a bad csrf token. This lets me customize the error page. - csrf_conf('callback', 'my_csrf_callback'); - - // While this is enabled by default to boost backwards compatibility, - // for security purposes it should ideally be off. Some users can be - // NATted or have dialup addresses which rotate frequently. Cookies - // are much more reliable. - csrf_conf('allow-ip', false); - - } - - // Finally, include the library - include_once '/path/to/csrf-magic.php'; - -Configuration gets stored in the $GLOBALS['csrf'] array. - - -4. THANKS - -My thanks to Chris Shiflett, for unintentionally inspiring the idea, as well -as telling me the original variant of the Bob and Mallory story, -and the Django CSRF Middleware authors, who thought up of this before me. -Gareth Heyes suggested using the frame-breaker option to protect against -CSS overlay attacks. diff --git a/csrf-conf.php b/csrf-conf.php new file mode 100644 index 0000000..0838eda --- /dev/null +++ b/csrf-conf.php @@ -0,0 +1,158 @@ + + */ +$GLOBALS['csrf']['input-name'] = '__csrf_magic'; + +/** + * Set this to false if your site must work inside of frame/iframe elements, + * but do so at your own risk: this configuration protects you against CSS + * overlay attacks that defeat tokens. + */ +$GLOBALS['csrf']['frame-breaker'] = true; + +/** + * Whether or not CSRF Magic should prefer using the session id as the + * primary method of generating a secured token for validating the post + * data + */ +$GLOBALS['csrf']['session'] = true; + +/** + * Whether or not CSRF Magic should be allowed to start a new session in order + * to determine the key. + */ +$GLOBALS['csrf']['auto-session'] = true; + +/** + * Whether or not csrf-magic should produce XHTML style tags. + */ +$GLOBALS['csrf']['xhtml'] = true; + +// FUNCTIONS: + +// Don't edit this! +$GLOBALS['csrf']['version'] = '1.1.0'; + +/** + * Where to log output to if we need to + */ +$GLOBALS['csrf']['log_file'] = ''; + +/** + * Whether to echo logging to the secreen + */ +$GLOBALS['csrf']['log_echo'] = ''; + +/** + * Path to secret file + */ +$GLOBALS['csrf']['path_secret'] = ''; + +/** + * Startup function, normally called csrf_startup() + */ +$GLOBALS['csrf']['startup_func'] = ''; + +/** + * Hashing function to use, defaults to sha1 + */ +$GLOBALS['csrf']['hash'] = ''; diff --git a/csrf-magic.js b/csrf-magic.js index 0989c10..7cacd6d 100644 --- a/csrf-magic.js +++ b/csrf-magic.js @@ -9,183 +9,188 @@ // The wrapper must be set BEFORE onreadystatechange is written to, since // a bug in ActiveXObject prevents us from properly testing for it. CsrfMagic = function(real) { - // try to make it ourselves, if you didn't pass it - if (!real) try { real = new XMLHttpRequest; } catch (e) {;} - if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) {;} - if (!real) try { real = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {;} - if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP.4.0'); } catch (e) {;} - this.csrf = real; - // properties - var csrfMagic = this; - real.onreadystatechange = function() { - csrfMagic._updateProps(); - return csrfMagic.onreadystatechange ? csrfMagic.onreadystatechange() : null; - }; - csrfMagic._updateProps(); + // try to make it ourselves, if you didn't pass it + if (!real) try { real = new XMLHttpRequest; } catch (e) {;} + if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) {;} + if (!real) try { real = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {;} + if (!real) try { real = new ActiveXObject('Msxml2.XMLHTTP.4.0'); } catch (e) {;} + this.csrf = real; + // properties + var csrfMagic = this; + real.onreadystatechange = function() { + csrfMagic._updateProps(); + return csrfMagic.onreadystatechange ? csrfMagic.onreadystatechange() : null; + }; + csrfMagic._updateProps(); } CsrfMagic.prototype = { - open: function(method, url, async, username, password) { - if (method == 'POST') this.csrf_isPost = true; - // deal with Opera bug, thanks jQuery - if (username) return this.csrf_open(method, url, async, username, password); - else return this.csrf_open(method, url, async); - }, - csrf_open: function(method, url, async, username, password) { - if (username) return this.csrf.open(method, url, async, username, password); - else return this.csrf.open(method, url, async); - }, + open: function(method, url, async, username, password) { + if (method == 'POST') this.csrf_isPost = true; + // deal with Opera bug, thanks jQuery + if (username) return this.csrf_open(method, url, async, username, password); + else return this.csrf_open(method, url, async); + }, + csrf_open: function(method, url, async, username, password) { + if (username) return this.csrf.open(method, url, async, username, password); + else return this.csrf.open(method, url, async); + }, - send: function(data) { - if (!this.csrf_isPost) return this.csrf_send(data); - prepend = csrfMagicName + '=' + csrfMagicToken + '&'; - // XXX: Removed to eliminate 'Refused to set unsafe header "Content-length" ' errors in modern browsers - // if (this.csrf_purportedLength === undefined) { - // this.csrf_setRequestHeader("Content-length", this.csrf_purportedLength + prepend.length); - // delete this.csrf_purportedLength; - // } - delete this.csrf_isPost; - return this.csrf_send(prepend + data); - }, - csrf_send: function(data) { - return this.csrf.send(data); - }, + send: function(data) { + if (!this.csrf_isPost) return this.csrf_send(data); + prepend = csrfMagicName + '=' + csrfMagicToken + '&'; + delete this.csrf_isPost; - setRequestHeader: function(header, value) { - // We have to auto-set this at the end, since we don't know how long the - // nonce is when added to the data. - if (this.csrf_isPost && header == "Content-length") { - this.csrf_purportedLength = value; - return; - } - return this.csrf_setRequestHeader(header, value); - }, - csrf_setRequestHeader: function(header, value) { - return this.csrf.setRequestHeader(header, value); - }, + if (typeof data == 'object') { + prepend = data; + prepend[csrfMagicName] = csrfMagicToken; + } else { + prepend = csrfMagicName + '=' + csrfMagicToken; + if (data) prepend = prepend + '&' + data; + } + return this.csrf_send(prepend); + }, + csrf_send: function(data) { + return this.csrf.send(data); + }, - abort: function() { - return this.csrf.abort(); - }, - getAllResponseHeaders: function() { - return this.csrf.getAllResponseHeaders(); - }, - getResponseHeader: function(header) { - return this.csrf.getResponseHeader(header); - } // , + setRequestHeader: function(header, value) { + // We have to auto-set this at the end, since we don't know how long the + // nonce is when added to the data. + if (this.csrf_isPost && header == "Content-length") { + this.csrf_purportedLength = value; + return; + } + return this.csrf_setRequestHeader(header, value); + }, + csrf_setRequestHeader: function(header, value) { + return this.csrf.setRequestHeader(header, value); + }, + + abort: function() { + return this.csrf.abort(); + }, + getAllResponseHeaders: function() { + return this.csrf.getAllResponseHeaders(); + }, + getResponseHeader: function(header) { + return this.csrf.getResponseHeader(header); + } // , } // proprietary CsrfMagic.prototype._updateProps = function() { - this.readyState = this.csrf.readyState; - if (this.readyState == 4) { - this.responseText = this.csrf.responseText; - this.responseXML = this.csrf.responseXML; - this.status = this.csrf.status; - this.statusText = this.csrf.statusText; - } + this.readyState = this.csrf.readyState; + if (this.readyState == 4) { + this.responseText = this.csrf.responseText; + this.responseXML = this.csrf.responseXML; + this.status = this.csrf.status; + this.statusText = this.csrf.statusText; + } } CsrfMagic.process = function(base) { - if(typeof base == 'object') { - base[csrfMagicName] = csrfMagicToken; - return base; - } - var prepend = csrfMagicName + '=' + csrfMagicToken; - if (base) return prepend + '&' + base; - return prepend; + if (typeof base == 'object') { + prepend = base; + prepend[csrfMagicName] = csrfMagicToken; + } else { + var prepend = csrfMagicName + '=' + csrfMagicToken; + if (base) prepend = prepend + '&' + base; + } + return prepend; } // callback function for when everything on the page has loaded CsrfMagic.end = function() { - // This rewrites forms AGAIN, so in case buffering didn't work this - // certainly will. - 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); - } + // This rewrites forms AGAIN, so in case buffering didn't work this + // certainly will. + 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 // We very specifically match against Internet Explorer, since they haven't // implemented prototypes correctly yet. if (window.XMLHttpRequest && window.XMLHttpRequest.prototype && '\v' != 'v') { - var x = XMLHttpRequest.prototype; - var c = CsrfMagic.prototype; + var x = XMLHttpRequest.prototype; + var c = CsrfMagic.prototype; - // Save the original functions - x.csrf_open = x.open; - x.csrf_send = x.send; - x.csrf_setRequestHeader = x.setRequestHeader; + // Save the original functions + x.csrf_open = x.open; + x.csrf_send = x.send; + x.csrf_setRequestHeader = x.setRequestHeader; - // Notice that CsrfMagic is itself an instantiatable object, but only - // open, send and setRequestHeader are necessary as decorators. - x.open = c.open; - x.send = c.send; - x.setRequestHeader = c.setRequestHeader; + // Notice that CsrfMagic is itself an instantiatable object, but only + // open, send and setRequestHeader are necessary as decorators. + x.open = c.open; + x.send = c.send; + x.setRequestHeader = c.setRequestHeader; } else { - // The only way we can do this is by modifying a library you have been - // 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 - // to do this the hard way. - jQuery.csrf_ajax = jQuery.ajax; - jQuery.ajax = function( s ) { - if (s.type && s.type.toUpperCase() == 'POST') { - s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s)); - if ( s.data && s.processData && typeof s.data != "string" ) { - s.data = jQuery.param(s.data); - } - s.data = CsrfMagic.process(s.data); - } - return jQuery.csrf_ajax( s ); - } - } - if (window.Prototype) { - // This works for script.aculo.us too - Ajax.csrf_getTransport = Ajax.getTransport; - Ajax.getTransport = function() { - return new CsrfMagic(Ajax.csrf_getTransport()); - } - } - if (window.MooTools) { - Browser.csrf_Request = Browser.Request; - Browser.Request = function () { - return new CsrfMagic(Browser.csrf_Request()); - } - } - if (window.YAHOO) { - // old YUI API - YAHOO.util.Connect.csrf_createXhrObject = YAHOO.util.Connect.createXhrObject; - YAHOO.util.Connect.createXhrObject = function (transaction) { - obj = YAHOO.util.Connect.csrf_createXhrObject(transaction); - obj.conn = new CsrfMagic(obj.conn); - return obj; - } - } - if (window.Ext) { - // Ext can use other js libraries as loaders, so it has to come last - // Ext's implementation is pretty identical to Yahoo's, but we duplicate - // it for comprehensiveness's sake. - Ext.lib.Ajax.csrf_createXhrObject = Ext.lib.Ajax.createXhrObject; - Ext.lib.Ajax.createXhrObject = function (transaction) { - obj = Ext.lib.Ajax.csrf_createXhrObject(transaction); - obj.conn = new CsrfMagic(obj.conn); - return obj; - } - } - if (window.dojo) { - // NOTE: this doesn't work with latest dojo - dojo.csrf__xhrObj = dojo._xhrObj; - dojo._xhrObj = function () { - return new CsrfMagic(dojo.csrf__xhrObj()); - } - } + // The only way we can do this is by modifying a library you have been + // 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 + // to do this the hard way. + jQuery.csrf_ajax = jQuery.ajax; + jQuery.ajax = function( s ) { + if (s.type && s.type.toUpperCase() == 'POST') { + s = jQuery.extend(true, s, jQuery.extend(true, {}, jQuery.ajaxSettings, s)); + if ( s.data && s.processData && typeof s.data != "string" ) { + s.data = jQuery.param(s.data); + } + s.data = CsrfMagic.process(s.data); + } + return jQuery.csrf_ajax( s ); + } + } + if (window.Prototype) { + // This works for script.aculo.us too + Ajax.csrf_getTransport = Ajax.getTransport; + Ajax.getTransport = function() { + return new CsrfMagic(Ajax.csrf_getTransport()); + } + } + if (window.MooTools) { + Browser.csrf_Request = Browser.Request; + Browser.Request = function () { + return new CsrfMagic(Browser.csrf_Request()); + } + } + if (window.YAHOO) { + // old YUI API + YAHOO.util.Connect.csrf_createXhrObject = YAHOO.util.Connect.createXhrObject; + YAHOO.util.Connect.createXhrObject = function (transaction) { + obj = YAHOO.util.Connect.csrf_createXhrObject(transaction); + obj.conn = new CsrfMagic(obj.conn); + return obj; + } + } + if (window.Ext) { + // Ext can use other js libraries as loaders, so it has to come last + // Ext's implementation is pretty identical to Yahoo's, but we duplicate + // it for comprehensiveness's sake. + Ext.lib.Ajax.csrf_createXhrObject = Ext.lib.Ajax.createXhrObject; + Ext.lib.Ajax.createXhrObject = function (transaction) { + obj = Ext.lib.Ajax.csrf_createXhrObject(transaction); + obj.conn = new CsrfMagic(obj.conn); + return obj; + } + } + if (window.dojo) { + // NOTE: this doesn't work with latest dojo + dojo.csrf__xhrObj = dojo._xhrObj; + dojo._xhrObj = function () { + return new CsrfMagic(dojo.csrf__xhrObj()); + } + } } + diff --git a/csrf-magic.php b/csrf-magic.php index 65db19f..84d6db9 100644 --- a/csrf-magic.php +++ b/csrf-magic.php @@ -10,198 +10,106 @@ * in every page), and forget about it! (There are, of course, configuration * options for advanced users). * - * This library is PHP4 and PHP5 compatible. + * This library is PHP4 and PHP5 compatible and is maintained from + * https://github.com/ezyang/csrf-magic/ */ -// CONFIGURATION: - -/** - * By default, when you include this file csrf-magic will automatically check - * and exit if the CSRF token is invalid. This will defer executing - * csrf_check() until you're ready. You can also pass false as a parameter to - * that function, in which case the function will not exit but instead return - * a boolean false if the CSRF check failed. This allows for tighter integration - * with your system. - */ -$GLOBALS['csrf']['defer'] = false; - -/** - * This is the amount of seconds you wish to allow before any token becomes - * invalid; the default is two hours, which should be more than enough for - * most websites. - */ -$GLOBALS['csrf']['expires'] = 7200; - -/** - * Callback function to execute when there's the CSRF check fails and - * $fatal == true (see csrf_check). This will usually output an error message - * about the failure. - */ -$GLOBALS['csrf']['callback'] = 'csrf_callback'; - -/** - * Whether or not to include our JavaScript library which also rewrites - * AJAX requests on this domain. Set this to the web path. This setting only works - * with supported JavaScript libraries in Internet Explorer; see README.txt for - * a list of supported libraries. - */ -$GLOBALS['csrf']['rewrite-js'] = false; - /** - * A secret key used when hashing items. Please generate a random string and - * place it here. If you change this value, all previously generated tokens - * will become invalid. - */ -$GLOBALS['csrf']['secret'] = ''; -// nota bene: library code should use csrf_get_secret() and not access -// this global directly - -/** - * 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 - * of valid keys. - */ -$GLOBALS['csrf']['allow-ip'] = true; - -/** - * If this information is available, use the cookie by this name to determine - * whether or not to allow the request. This is a shortcut implementation - * very similar to 'key', but we randomly set the cookie ourselves. + * Rewrites
on the fly to add CSRF tokens to them. This can also + * inject our JavaScript library. */ -$GLOBALS['csrf']['cookie'] = '__csrf_cookie'; +function csrf_ob_handler($buffer, $flags) { + // Even though the user told us to rewrite, we should do a quick heuristic + // to check if the page is *actually* HTML. We don't begin rewriting until + // we hit the first "; + $buffer = preg_replace('#(]*method\s*=\s*["\']post["\'][^>]*>)#i', '$1' . $input, $buffer); + + if ($GLOBALS['csrf']['frame-breaker']) { + $buffer = str_ireplace('', '', $buffer); + } + + $js = $GLOBALS['csrf']['rewrite-js']; + + if (!empty($js)) { + $buffer = str_ireplace( + '', + ''. + '', + $buffer + ); + + $script = ''; + $buffer = str_ireplace('', $script . '', $buffer, $count); + + if (!$count) { + $buffer .= $script; + } + } + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($buffer, true)); + + return $buffer; +} /** - * If this information is available, set this to a unique identifier (it - * can be an integer or a unique username) for the current "user" of this - * application. The token will then be globally valid for all of that user's - * operations, but no one else. This requires that 'secret' be set. + * Checks if this is a post request, and if it is, checks if the nonce is valid. + * @param bool $fatal Whether or not to fatally error out if there is a problem. + * @return True if check passes or is not necessary, false if failure. */ -$GLOBALS['csrf']['user'] = false; +function csrf_check($fatal = true) { + $result = true; + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'POST') { + $result = false; + csrf_start(); -/** - * This is an arbitrary secret value associated with the user's session. This - * will most probably be the contents of a cookie, as an attacker cannot easily - * determine this information. Warning: If the attacker knows this value, they - * can easily spoof a token. This is a generic implementation; sessions should - * work in most cases. - * - * Why would you want to use this? Lets suppose you have a squid cache for your - * website, and the presence of a session cookie bypasses it. Let's also say - * you allow anonymous users to interact with the website; submitting forms - * and AJAX. Previously, you didn't have any CSRF protection for anonymous users - * and so they never got sessions; you don't want to start using sessions either, - * otherwise you'll bypass the Squid cache. Setup a different cookie for CSRF - * tokens, and have Squid ignore that cookie for get requests, for anonymous - * users. (If you haven't guessed, this scheme was(?) used for MediaWiki). - */ -$GLOBALS['csrf']['key'] = false; + $name = $GLOBALS['csrf']['input-name']; + $result = isset($_POST[$name]); + $tokens = ''; -/** - * The name of the magic CSRF token that will be placed in all forms, i.e. - * the contents of - */ -$GLOBALS['csrf']['input-name'] = '__csrf_magic'; + csrf_log(__FUNCTION__, "csrf magic $name was $result"); -/** - * Set this to false if your site must work inside of frame/iframe elements, - * but do so at your own risk: this configuration protects you against CSS - * overlay attacks that defeat tokens. - */ -$GLOBALS['csrf']['frame-breaker'] = true; + if ($result) { + // we don't regenerate a token and check it because some token creation + // schemes are volatile. + $tokens = $_POST[$name]; + $result = csrf_check_tokens($tokens); + if (is_array($tokens)) { + $tokens = implode(';', $tokens); + } -/** - * Whether or not CSRF Magic should be allowed to start a new session in order - * to determine the key. - */ -$GLOBALS['csrf']['auto-session'] = true; + csrf_log(__FUNCTION__, "check_tokens($name, $tokens) returned $result"); + } -/** - * Whether or not csrf-magic should produce XHTML style tags. - */ -$GLOBALS['csrf']['xhtml'] = true; + if ($fatal && !$result) { + $callback = $GLOBALS['csrf']['callback']; -// FUNCTIONS: + // filter tokens to ensure only valid tokens passed + if (trim($tokens, 'A..Za..z0..9:;,') !== '') { + $tokens = 'hidden'; + } -// Don't edit this! -$GLOBALS['csrf']['version'] = '1.0.4'; + $callback($tokens); + exit; + } + } -/** - * Rewrites on the fly to add CSRF tokens to them. This can also - * inject our JavaScript library. - */ -function csrf_ob_handler($buffer, $flags) { - // Even though the user told us to rewrite, we should do a quick heuristic - // to check if the page is *actually* HTML. We don't begin rewriting until - // we hit the first "; - $buffer = preg_replace('#(]*method\s*=\s*["\']post["\'][^>]*>)#i', '$1' . $input, $buffer); - if ($GLOBALS['csrf']['frame-breaker']) { - $buffer = str_ireplace('', '', $buffer); - } - if ($js = $GLOBALS['csrf']['rewrite-js']) { - $buffer = str_ireplace( - '', - ''. - '', - $buffer - ); - $script = ''; - $buffer = str_ireplace('', $script . '', $buffer, $count); - if (!$count) { - $buffer .= $script; - } - } - return $buffer; -} + csrf_log(__FUNCTION__, 'returns: ' . var_export($result, true)); -/** - * Checks if this is a post request, and if it is, checks if the nonce is valid. - * @param bool $fatal Whether or not to fatally error out if there is a problem. - * @return True if check passes or is not necessary, false if failure. - */ -function csrf_check($fatal = true) { - if ($_SERVER['REQUEST_METHOD'] !== 'POST') return true; - csrf_start(); - $name = $GLOBALS['csrf']['input-name']; - $ok = false; - $tokens = ''; - do { - if (!isset($_POST[$name])) break; - // we don't regenerate a token and check it because some token creation - // schemes are volatile. - $tokens = $_POST[$name]; - if (!csrf_check_tokens($tokens)) break; - $ok = true; - } while (false); - if ($fatal && !$ok) { - $callback = $GLOBALS['csrf']['callback']; - if (trim($tokens, 'A..Za..z0..9:;,') !== '') $tokens = 'hidden'; - $callback($tokens); - exit; - } - return $ok; + return $result; } /** @@ -209,75 +117,102 @@ function csrf_check($fatal = true) { * by semicolons. */ function csrf_get_tokens() { - $has_cookies = !empty($_COOKIE); - - // $ip implements a composite key, which is sent if the user hasn't sent - // any cookies. It may or may not be used, depending on whether or not - // the cookies "stick" - $secret = csrf_get_secret(); - if (!$has_cookies && $secret) { - // :TODO: Harden this against proxy-spoofing attacks - $IP_ADDRESS = (isset($_SERVER['IP_ADDRESS']) ? $_SERVER['IP_ADDRESS'] : $_SERVER['REMOTE_ADDR']); - $ip = ';ip:' . csrf_hash($IP_ADDRESS); - } else { - $ip = ''; - } - csrf_start(); - - // These are "strong" algorithms that don't require per se a secret - if (session_id()) return 'sid:' . csrf_hash(session_id()) . $ip; - if ($GLOBALS['csrf']['cookie']) { - $val = csrf_generate_secret(); - setcookie($GLOBALS['csrf']['cookie'], $val); - return 'cookie:' . csrf_hash($val) . $ip; - } - if ($GLOBALS['csrf']['key']) return 'key:' . csrf_hash($GLOBALS['csrf']['key']) . $ip; - // These further algorithms require a server-side secret - if (!$secret) return 'invalid'; - if ($GLOBALS['csrf']['user'] !== false) { - return 'user:' . csrf_hash($GLOBALS['csrf']['user']); - } - if ($GLOBALS['csrf']['allow-ip']) { - return ltrim($ip, ';'); - } - return 'invalid'; + $has_cookies = !empty($_COOKIE); + + // $ip implements a composite key, which is sent if the user hasn't sent + // any cookies. It may or may not be used, depending on whether or not + // the cookies "stick" + $secret = csrf_get_secret(); + $token = ''; + $ip = ''; + + if (!$has_cookies && $secret) { + $ip = csrf_get_client_addr(); + if (!empty($ip)) { + $ip = ';ip:' . csrf_hash($_SERVER['REMOTE_ADDR']); + } + } + + csrf_start(); + + // These are "strong" algorithms that don't require per se a secret + if ($GLOBALS['csrf']['session'] && session_id()) { + $token = 'sid:' . csrf_hash(session_id()) . $ip; + } elseif ($GLOBALS['csrf']['cookie']) { + $val = csrf_generate_secret(); + setcookie($GLOBALS['csrf']['cookie'], $val, time() + 3600, $GLOBALS['csrf']['url_path']); + $token = 'cookie:' . csrf_hash($val) . $ip; + } elseif ($GLOBALS['csrf']['key']) { + $token = 'key:' . csrf_hash($GLOBALS['csrf']['key']) . $ip; + } elseif (!$secret) { + $token = 'invalid'; + } elseif ($GLOBALS['csrf']['user'] !== false) { + $token = 'user:' . csrf_hash($GLOBALS['csrf']['user']); + } elseif ($GLOBALS['csrf']['allow-ip']) { + $token = ltrim($ip, ';'); + } else { + $token = 'invalid'; + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($token, true)); + + return $token; } function csrf_flattenpost($data) { - $ret = array(); - foreach($data as $n => $v) { - $ret = array_merge($ret, csrf_flattenpost2(1, $n, $v)); - } - return $ret; + $ret = array(); + foreach($data as $n => $v) { + $ret = array_merge($ret, csrf_flattenpost2(1, $n, $v)); + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($ret, true)); + + return $ret; } + function csrf_flattenpost2($level, $key, $data) { - if(!is_array($data)) return array($key => $data); - $ret = array(); - foreach($data as $n => $v) { - $nk = $level >= 1 ? $key."[$n]" : "[$n]"; - $ret = array_merge($ret, csrf_flattenpost2($level+1, $nk, $v)); - } - return $ret; + if(!is_array($data)) { + $ret = array($key => $data); + } else { + $ret = array(); + foreach($data as $n => $v) { + $nk = $level >= 1 ? $key."[$n]" : "[$n]"; + $ret = array_merge($ret, csrf_flattenpost2($level+1, $nk, $v)); + } + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($ret, true)); + + return $ret; } /** * @param $tokens is safe for HTML consumption */ function csrf_callback($tokens) { - // (yes, $tokens is safe to echo without escaping) - header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden'); - $data = ''; - foreach (csrf_flattenpost($_POST) as $key => $value) { - if ($key == $GLOBALS['csrf']['input-name']) continue; - $data .= ''; - } - echo "CSRF check failed - -

CSRF check failed. Your form session may have expired, or you may not have - cookies enabled.

- $data -

Debug: $tokens

-"; + // (yes, $tokens is safe to echo without escaping) + $data = ''; + foreach (csrf_flattenpost($_POST) as $key => $value) { + if ($key != $GLOBALS['csrf']['input-name']) { + $data .= ''; + } + } + + echo " + + CSRF check failed + + +

+ + CSRF check failed. Your form session may have expired, or you may not have cookies + enabled. + +

+
$data
+

Debug: $tokens

+ +"; } /** @@ -285,107 +220,195 @@ function csrf_callback($tokens) { * instead of csrf_check_token() */ function csrf_check_tokens($tokens) { - if (is_string($tokens)) $tokens = explode(';', $tokens); - foreach ($tokens as $token) { - if (csrf_check_token($token)) return true; - } - return false; + if (is_string($tokens)) { + $tokens = explode(';', $tokens); + } + + $valid_token = false; + foreach ($tokens as $token) { + if (csrf_check_token($token)) { + $valid_token = true; + break; + } + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($valid_token, true)); + + return $valid_token; } /** * Checks if a token is valid. */ function csrf_check_token($token) { - if (strpos($token, ':') === false) return false; - list($type, $value) = explode(':', $token, 2); - if (strpos($value, ',') === false) return false; - list($x, $time) = explode(',', $token, 2); - if ($GLOBALS['csrf']['expires']) { - if (time() > $time + $GLOBALS['csrf']['expires']) return false; - } - switch ($type) { - case 'sid': - return $value === csrf_hash(session_id(), $time); - case 'cookie': - $n = $GLOBALS['csrf']['cookie']; - if (!$n) return false; - if (!isset($_COOKIE[$n])) return false; - return $value === csrf_hash($_COOKIE[$n], $time); - case 'key': - if (!$GLOBALS['csrf']['key']) return false; - return $value === csrf_hash($GLOBALS['csrf']['key'], $time); - // We could disable these 'weaker' checks if 'key' was set, but - // that doesn't make me feel good then about the cookie-based - // implementation. - case 'user': - if (!csrf_get_secret()) return false; - if ($GLOBALS['csrf']['user'] === false) return false; - return $value === csrf_hash($GLOBALS['csrf']['user'], $time); - case 'ip': - if (!csrf_get_secret()) return false; - // do not allow IP-based checks if the username is set, or if - // the browser sent cookies - if ($GLOBALS['csrf']['user'] !== false) return false; - if (!empty($_COOKIE)) return false; - if (!$GLOBALS['csrf']['allow-ip']) return false; - $IP_ADDRESS = (isset($_SERVER['IP_ADDRESS']) ? $_SERVER['IP_ADDRESS'] : $_SERVER['REMOTE_ADDR']); - return $value === csrf_hash($IP_ADDRESS, $time); - } - return false; + $valid_token = false; + if (strpos($token, ':') !== false) { + list($type, $value) = explode(':', $token, 2); + + if (strpos($value, ',') !== false) { + list($x, $time) = explode(',', $token, 2); + + $check_token = true; + if ($GLOBALS['csrf']['expires']) { + $expiry_time = time(); + $expiry_csrf = $time + $GLOBALS['csrf']['expires']; + $check_token = ($expiry_time < $expiry_csrf); + + csrf_log(__FUNCTION__, "expiry $check_token = $expiry_time < $expiry_csrf"); + } + + if ($check_token) { + switch ($type) { + case 'sid': + $valid_token = ($value === csrf_hash(session_id(), $time)); + break; + case 'cookie': + $n = $GLOBALS['csrf']['cookie']; + if ($n && isset($_COOKIE[$n])) { + $valid_token = ($value === csrf_hash($_COOKIE[$n], $time)); + } + break; + case 'key': + if ($GLOBALS['csrf']['key']) { + $valid_token = ($value === csrf_hash($GLOBALS['csrf']['key'], $time)); + } + break; + + // We could disable these 'weaker' checks if 'key' was set, but + // that doesn't make me feel good then about the cookie-based + // implementation. + case 'user': + if (csrf_get_secret() && $GLOBALS['csrf']['user'] !== false) { + $valid_token = ($value === csrf_hash($GLOBALS['csrf']['user'], $time)); + } + break; + case 'ip': + // do not allow IP-based checks if the username is set, or if + // the browser sent cookies + if (csrf_get_secret() && + $GLOBALS['csrf']['user'] === false && + empty($_COOKIE) && + $GLOBALS['csrf']['allow-ip']) { + + $client_ip = csrf_get_client_addr(); + if (!empty($client_ip)) { + $valid_token = ($value === csrf_hash($client_ip, $time)); + } + } + break; + } + + csrf_log(__FUNCTION__, 'Checking ' . $type . ' resulted ' . $valid_token); + } + } + } + + csrf_log(__FUNCTION__, 'returns: ' . var_export($valid_token, true)); + + return $valid_token; } /** * Sets a configuration value. */ function csrf_conf($key, $val) { - if (!isset($GLOBALS['csrf'][$key])) { - trigger_error('No such configuration ' . $key, E_USER_WARNING); - return; - } - $GLOBALS['csrf'][$key] = $val; + if (!isset($GLOBALS['csrf'][$key])) { + trigger_error('No such configuration ' . $key, E_USER_WARNING); + } else { + $old_val = $GLOBALS['csrf'][$key]; + $GLOBALS['csrf'][$key] = $val; + + //csrf_log(__FUNCTION__,'Configuration option [' . $key . '] set to [' . $val . '] (was [' . $old_val . '])'); + } } /** * Starts a session if we're allowed to. */ function csrf_start() { - if ($GLOBALS['csrf']['auto-session'] && !session_id()) { - session_start(); - } + global $config; + + if ($GLOBALS['csrf']['auto-session'] && !session_id()) { + session_start(); + } } /** * Retrieves the secret, and generates one if necessary. */ function csrf_get_secret() { - if ($GLOBALS['csrf']['secret']) return $GLOBALS['csrf']['secret']; - $dir = dirname(__FILE__); - $file = $dir . '/csrf-secret.php'; - $secret = ''; - if (file_exists($file)) { - include $file; - return $secret; - } - if (is_writable($dir)) { - $secret = csrf_generate_secret(); - $fh = fopen($file, 'w'); - fwrite($fh, 'Format('Y-m-d H:i:s.u'), $name, csrf_caller(), PHP_EOL, $text, PHP_EOL, PHP_EOL, csrf_backtrace('',0,2), PHP_EOL, PHP_EOL); + + file_put_contents($log_file, $l, FILE_APPEND); + if (!empty($GLOBALS['csrf']['log_echo'])) { + print $l; + } + } +} + +function csrf_caller() { + static $caller = ''; + + if (empty($caller)) { + if (!empty($_SERVER['REQUEST_URI'])) { + $caller = $_SERVER['REQUEST_URI']; + } else { + $caller = $_SERVER['SCRIPT_NAME']; + } + } + + return $caller; +} + +function csrf_backtrace($entry = '', $limit = 0, $skip = 0) { + global $config; + + $skip = $skip >= 0 ? $skip : 1; + $limit = $limit > 0 ? ($limit + $skip) : 0; + + $callers = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit); + while ($skip > 0) { + array_shift($callers); + $skip--; + } + + $s=''; + foreach ($callers as $c) { + if (isset($c['line'])) { + $line = '[' . $c['line'] . ']'; + } else { + $line = ''; + } + + if (isset($c['file'])) { + if (isset($config['base_path'])) { + $file = str_replace($config['base_path'], '', $c['file']) . $line; + } else { + $file = $c['file'] . $line; + } + } else { + $file = $line; + } + + $func = $c['function'].'()'; + if (isset($c['class'])) { + $func = $c['class'] . $c['type'] . $func; + } + + $s = sprintf('%30s : %s' . PHP_EOL, $func, $file) . $s; + } + + return $s; +} + +/****** MAIN CODE ******/ + +require_once(__DIR__ . '/csrf-conf.php'); + +if (!empty($GLOBALS['csrf']['startup'])) { + $csrf_startup_func = $GLOBALS['csrf']['startup']; +} elseif (function_exists('csrf_startup')) { + $csrf_startup_func = 'csrf_startup'; +} + +if (function_exists($csrf_startup_func)) { + call_user_func($csrf_startup_func); +} + +if (!empty($_POST)) { + csrf_log('', var_export($_POST, true)); +} + +if (!empty($_GET)) { + csrf_log('', var_export($_GET, true)); +} + +if (!$GLOBALS['csrf']['disable']) { + + // Initialize our handler + if ($GLOBALS['csrf']['rewrite']) { + ob_start('csrf_ob_handler'); + } + + // Perform check + if (!$GLOBALS['csrf']['defer']) { + csrf_check(); + } +} diff --git a/test.php b/test.php index 3e5f461..168b488 100644 --- a/test.php +++ b/test.php @@ -1,4 +1,4 @@ -