Merge pull request #19 from arkascha/master
Reimplementation of CSRF protection strategy
This commit is contained in:
commit
cfc9839812
1
AUTHORS
1
AUTHORS
|
@ -17,6 +17,7 @@ ownCloud is written by:
|
||||||
Sam Tuke
|
Sam Tuke
|
||||||
Simon Birnbach
|
Simon Birnbach
|
||||||
Lukas Reschke
|
Lukas Reschke
|
||||||
|
Christian Reiner
|
||||||
…
|
…
|
||||||
|
|
||||||
With help from many libraries and frameworks including:
|
With help from many libraries and frameworks including:
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* ownCloud
|
||||||
|
* @author Christian Reiner
|
||||||
|
* @copyright 2011-2012 Christian Reiner <foss@christian-reiner.info>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 3 of the license, or any later version.
|
||||||
|
*
|
||||||
|
* This library 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 this library.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file core/ajax/requesttoken.php
|
||||||
|
* @brief Ajax method to retrieve a fresh request protection token for ajax calls
|
||||||
|
* @return json: success/error state indicator including a fresh request token
|
||||||
|
* @author Christian Reiner
|
||||||
|
*/
|
||||||
|
require_once '../../lib/base.php';
|
||||||
|
|
||||||
|
// don't load apps or filesystem for this task
|
||||||
|
$RUNTIME_NOAPPS = TRUE;
|
||||||
|
$RUNTIME_NOSETUPFS = TRUE;
|
||||||
|
|
||||||
|
// Sanity checks
|
||||||
|
// using OCP\JSON::callCheck() below protects the token refreshing itself.
|
||||||
|
//OCP\JSON::callCheck ( );
|
||||||
|
OCP\JSON::checkLoggedIn ( );
|
||||||
|
// hand out a fresh token
|
||||||
|
OCP\JSON::success ( array ( 'token' => OCP\Util::callRegister() ) );
|
||||||
|
?>
|
|
@ -40,7 +40,7 @@ OC.EventSource=function(src,data){
|
||||||
dataStr+=name+'='+encodeURIComponent(data[name])+'&';
|
dataStr+=name+'='+encodeURIComponent(data[name])+'&';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataStr+='requesttoken='+OC.EventSource.requesttoken;
|
dataStr+='requesttoken='+OC.Request.Token;
|
||||||
if(!this.useFallBack && typeof EventSource !='undefined'){
|
if(!this.useFallBack && typeof EventSource !='undefined'){
|
||||||
this.source=new EventSource(src+'?'+dataStr);
|
this.source=new EventSource(src+'?'+dataStr);
|
||||||
this.source.onmessage=function(e){
|
this.source.onmessage=function(e){
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* ownCloud
|
||||||
|
*
|
||||||
|
* @file core/js/requesttoken.js
|
||||||
|
* @brief Routine to refresh the Request protection request token periodically
|
||||||
|
* @author Christian Reiner (arkascha)
|
||||||
|
* @copyright 2011-2012 Christian Reiner <foss@christian-reiner.info>
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 3 of the license, or any later version.
|
||||||
|
*
|
||||||
|
* This library 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 this library.
|
||||||
|
* If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
OC.Request = {
|
||||||
|
// the request token
|
||||||
|
Token: {},
|
||||||
|
// the lifespan span (in secs)
|
||||||
|
Lifespan: {},
|
||||||
|
// method to refresh the local request token periodically
|
||||||
|
Refresh: function(){
|
||||||
|
// just a client side console log to preserve efficiency
|
||||||
|
console.log("refreshing request token (lifebeat)");
|
||||||
|
var dfd=new $.Deferred();
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: OC.filePath('core','ajax','requesttoken.php'),
|
||||||
|
cache: false,
|
||||||
|
data: { },
|
||||||
|
dataType: 'json'
|
||||||
|
}).done(function(response){
|
||||||
|
// store refreshed token inside this class
|
||||||
|
OC.Request.Token=response.token;
|
||||||
|
dfd.resolve();
|
||||||
|
}).fail(dfd.reject);
|
||||||
|
return dfd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// accept requesttoken and lifespan into the OC namespace
|
||||||
|
OC.Request.Token = oc_requesttoken;
|
||||||
|
OC.Request.Lifespan = oc_requestlifespan;
|
||||||
|
// refresh the request token periodically shortly before it becomes invalid on the server side
|
||||||
|
setInterval(OC.Request.Refresh,Math.floor(1000*OC.Request.Lifespan*0.93)), // 93% of lifespan value, close to when the token expires
|
||||||
|
// early bind token as additional ajax argument for every single request
|
||||||
|
$(document).bind('ajaxSend', function(elm, xhr, s){xhr.setRequestHeader('requesttoken', OC.Request.Token);});
|
|
@ -11,6 +11,8 @@
|
||||||
var oc_webroot = '<?php echo OC::$WEBROOT; ?>';
|
var oc_webroot = '<?php echo OC::$WEBROOT; ?>';
|
||||||
var oc_appswebroots = <?php echo $_['apps_paths'] ?>;
|
var oc_appswebroots = <?php echo $_['apps_paths'] ?>;
|
||||||
var oc_current_user = '<?php echo OC_User::getUser() ?>';
|
var oc_current_user = '<?php echo OC_User::getUser() ?>';
|
||||||
|
var oc_requesttoken = '<?php echo $_['requesttoken']; ?>';
|
||||||
|
var oc_requestlifespan = '<?php echo $_['requestlifespan']; ?>';
|
||||||
</script>
|
</script>
|
||||||
<?php foreach($_['jsfiles'] as $jsfile): ?>
|
<?php foreach($_['jsfiles'] as $jsfile): ?>
|
||||||
<script type="text/javascript" src="<?php echo $jsfile; ?>"></script>
|
<script type="text/javascript" src="<?php echo $jsfile; ?>"></script>
|
||||||
|
@ -24,13 +26,6 @@
|
||||||
echo '/>';
|
echo '/>';
|
||||||
?>
|
?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<script type="text/javascript">
|
|
||||||
requesttoken = '<?php echo $_['requesttoken']; ?>';
|
|
||||||
OC.EventSource.requesttoken=requesttoken;
|
|
||||||
$(document).bind('ajaxSend', function(elm, xhr, s) {
|
|
||||||
xhr.setRequestHeader('requesttoken', requesttoken);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="<?php echo $_['bodyid'];?>">
|
<body id="<?php echo $_['bodyid'];?>">
|
||||||
|
|
|
@ -240,6 +240,8 @@ class OC{
|
||||||
OC_Util::addScript( "jquery-tipsy" );
|
OC_Util::addScript( "jquery-tipsy" );
|
||||||
OC_Util::addScript( "oc-dialogs" );
|
OC_Util::addScript( "oc-dialogs" );
|
||||||
OC_Util::addScript( "js" );
|
OC_Util::addScript( "js" );
|
||||||
|
// request protection token MUST be defined after the jquery library but before any $('document').ready()
|
||||||
|
OC_Util::addScript( "requesttoken" );
|
||||||
OC_Util::addScript( "eventsource" );
|
OC_Util::addScript( "eventsource" );
|
||||||
OC_Util::addScript( "config" );
|
OC_Util::addScript( "config" );
|
||||||
//OC_Util::addScript( "multiselect" );
|
//OC_Util::addScript( "multiselect" );
|
||||||
|
|
|
@ -157,6 +157,7 @@ class OC_Template{
|
||||||
$this->vars = array();
|
$this->vars = array();
|
||||||
if($renderas == 'user') {
|
if($renderas == 'user') {
|
||||||
$this->vars['requesttoken'] = OC_Util::callRegister();
|
$this->vars['requesttoken'] = OC_Util::callRegister();
|
||||||
|
$this->vars['requestlifespan'] = OC_Util::$callLifespan;
|
||||||
}
|
}
|
||||||
$parts = explode('/', $app); // fix translation when app is something like core/lostpassword
|
$parts = explode('/', $app); // fix translation when app is something like core/lostpassword
|
||||||
$this->l10n = OC_L10N::get($parts[0]);
|
$this->l10n = OC_L10N::get($parts[0]);
|
||||||
|
@ -374,6 +375,7 @@ class OC_Template{
|
||||||
$page = new OC_TemplateLayout($this->renderas);
|
$page = new OC_TemplateLayout($this->renderas);
|
||||||
if($this->renderas == 'user') {
|
if($this->renderas == 'user') {
|
||||||
$page->assign('requesttoken', $this->vars['requesttoken']);
|
$page->assign('requesttoken', $this->vars['requesttoken']);
|
||||||
|
$page->assign('requestlifespan', $this->vars['requestlifespan']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom headers
|
// Add custom headers
|
||||||
|
|
36
lib/util.php
36
lib/util.php
|
@ -416,14 +416,29 @@ class OC_Util {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Register an get/post call. This is important to prevent CSRF attacks
|
* @brief Static lifespan (in seconds) when a request token expires.
|
||||||
* Todo: Write howto
|
* @see OC_Util::callRegister()
|
||||||
|
* @see OC_Util::isCallRegistered()
|
||||||
|
* @description
|
||||||
|
* Also required for the client side to compute the piont in time when to
|
||||||
|
* request a fresh token. The client will do so when nearly 97% of the
|
||||||
|
* timespan coded here has expired.
|
||||||
|
*/
|
||||||
|
public static $callLifespan = 3600; // 3600 secs = 1 hour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register an get/post call. Important to prevent CSRF attacks.
|
||||||
|
* @todo Write howto: CSRF protection guide
|
||||||
* @return $token Generated token.
|
* @return $token Generated token.
|
||||||
|
* @description
|
||||||
|
* Creates a 'request token' (random) and stores it inside the session.
|
||||||
|
* Ever subsequent (ajax) request must use such a valid token to succeed,
|
||||||
|
* otherwise the request will be denied as a protection against CSRF.
|
||||||
|
* The tokens expire after a fixed lifespan.
|
||||||
|
* @see OC_Util::$callLifespan
|
||||||
|
* @see OC_Util::isCallRegistered()
|
||||||
*/
|
*/
|
||||||
public static function callRegister() {
|
public static function callRegister() {
|
||||||
//mamimum time before token exires
|
|
||||||
$maxtime=(60*60); // 1 hour
|
|
||||||
|
|
||||||
// generate a random token.
|
// generate a random token.
|
||||||
$token=mt_rand(1000,9000).mt_rand(1000,9000).mt_rand(1000,9000);
|
$token=mt_rand(1000,9000).mt_rand(1000,9000).mt_rand(1000,9000);
|
||||||
|
|
||||||
|
@ -436,7 +451,8 @@ class OC_Util {
|
||||||
foreach($_SESSION as $key=>$value) {
|
foreach($_SESSION as $key=>$value) {
|
||||||
// search all tokens in the session
|
// search all tokens in the session
|
||||||
if(substr($key,0,12)=='requesttoken') {
|
if(substr($key,0,12)=='requesttoken') {
|
||||||
if($value+$maxtime<time()) {
|
// check if static lifespan has expired
|
||||||
|
if($value+self::$callLifespan<time()) {
|
||||||
// remove outdated tokens
|
// remove outdated tokens
|
||||||
unset($_SESSION[$key]);
|
unset($_SESSION[$key]);
|
||||||
}
|
}
|
||||||
|
@ -447,14 +463,13 @@ class OC_Util {
|
||||||
return($token);
|
return($token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Check an ajax get/post call if the request token is valid.
|
* @brief Check an ajax get/post call if the request token is valid.
|
||||||
* @return boolean False if request token is not set or is invalid.
|
* @return boolean False if request token is not set or is invalid.
|
||||||
|
* @see OC_Util::$callLifespan
|
||||||
|
* @see OC_Util::calLRegister()
|
||||||
*/
|
*/
|
||||||
public static function isCallRegistered() {
|
public static function isCallRegistered() {
|
||||||
//mamimum time before token exires
|
|
||||||
$maxtime=(60*60); // 1 hour
|
|
||||||
if(isset($_GET['requesttoken'])) {
|
if(isset($_GET['requesttoken'])) {
|
||||||
$token=$_GET['requesttoken'];
|
$token=$_GET['requesttoken'];
|
||||||
}elseif(isset($_POST['requesttoken'])) {
|
}elseif(isset($_POST['requesttoken'])) {
|
||||||
|
@ -467,7 +482,8 @@ class OC_Util {
|
||||||
}
|
}
|
||||||
if(isset($_SESSION['requesttoken-'.$token])) {
|
if(isset($_SESSION['requesttoken-'.$token])) {
|
||||||
$timestamp=$_SESSION['requesttoken-'.$token];
|
$timestamp=$_SESSION['requesttoken-'.$token];
|
||||||
if($timestamp+$maxtime<time()) {
|
// check if static lifespan has expired
|
||||||
|
if($timestamp+self::$callLifespan<time()) {
|
||||||
return false;
|
return false;
|
||||||
}else{
|
}else{
|
||||||
//token valid
|
//token valid
|
||||||
|
|
Loading…
Reference in New Issue