diff --git a/common/framework/security.php b/common/framework/security.php
index 751a8c4b0..f5591ed1b 100644
--- a/common/framework/security.php
+++ b/common/framework/security.php
@@ -307,16 +307,35 @@ class Security
*/
public static function checkCSRF($referer = null)
{
- if (!$referer)
- {
- $referer = strval($_SERVER['HTTP_REFERER']);
- }
- if (strval($referer) === '')
+ if ($_SERVER['REQUEST_METHOD'] === 'GET')
{
return true;
}
-
- return URL::isInternalURL($referer);
+ elseif ($token = $_SERVER['HTTP_X_CSRF_TOKEN'])
+ {
+ return Session::verifyToken($token);
+ }
+ elseif ($token = \Context::get('_rx_csrf_token'))
+ {
+ return Session::verifyToken($token);
+ }
+ else
+ {
+ if (Session::getMemberSrl())
+ {
+ trigger_error('CSRF token missing in POST request: ' . (\Context::get('act') ?: '(no act)'), \E_USER_WARNING);
+ }
+
+ $referer = strval($referer ?: $_SERVER['HTTP_REFERER']);
+ if ($referer !== '')
+ {
+ return URL::isInternalURL($referer);
+ }
+ else
+ {
+ return false;
+ }
+ }
}
/**
diff --git a/common/framework/session.php b/common/framework/session.php
index b3643473c..33c44bd9f 100644
--- a/common/framework/session.php
+++ b/common/framework/session.php
@@ -378,6 +378,7 @@ class Session
$_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser();
$_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum');
$_SESSION['RHYMIX']['tokens'] = array();
+ $_SESSION['RHYMIX']['token'] = false;
$_SESSION['is_webview'] = self::_isBuggyUserAgent();
$_SESSION['is_new_session'] = true;
$_SESSION['is_logged'] = false;
@@ -847,6 +848,26 @@ class Session
}
}
+ /**
+ * Get a generic token that is not restricted to any particular key.
+ *
+ * @return string|false
+ */
+ public static function getGenericToken()
+ {
+ if (!self::isStarted())
+ {
+ return false;
+ }
+
+ if (!$_SESSION['RHYMIX']['token'])
+ {
+ $_SESSION['RHYMIX']['token'] = self::createToken('');
+ }
+
+ return $_SESSION['RHYMIX']['token'];
+ }
+
/**
* Create a token that can only be verified in the same session.
*
diff --git a/common/js/common.js b/common/js/common.js
index 7198d0370..d21b5df8a 100644
--- a/common/js/common.js
+++ b/common/js/common.js
@@ -17,7 +17,7 @@
($.os.Linux) ? 'Linux' :
($.os.Unix) ? 'Unix' :
($.os.Mac) ? 'Mac' : '';
-
+
/* Intercept getScript error due to broken minified script URL */
$(document).ajaxError(function(event, jqxhr, settings, thrownError) {
if(settings.dataType === "script" && (jqxhr.status >= 400 || (jqxhr.responseText && jqxhr.responseText.length < 40))) {
@@ -27,7 +27,56 @@
}
}
});
+
+ /**
+ * @brief Check if two URLs belong to the same origin
+ */
+ window.isSameOrigin = function(url1, url2, allow_relative_url2) {
+ var a1 = $("").attr("href", url1)[0];
+ var a2 = $("").attr("href", url2)[0];
+ if (!a2.hostname && allow_relative_url2) {
+ return true;
+ }
+ if (a1.protocol !== a2.protocol) return false;
+ if (a1.hostname !== a2.hostname) return false;
+ if (a1.port !== a2.port) return false;
+ return true;
+ };
+ /**
+ * @brief Get CSRF token for the document
+ */
+ window.getCSRFToken = function() {
+ return $("meta[name='csrf-token']").attr("content");
+ };
+
+ /* Intercept jQuery AJAX calls to add CSRF headers */
+ $.ajaxPrefilter(function(options) {
+ if (!isSameOrigin(location.href, options.url, true)) return;
+ var token = getCSRFToken();
+ if (token) {
+ if (!options.headers) options.headers = {};
+ options.headers["X-CSRF-Token"] = token;
+ }
+ });
+
+ /* Add CSRF token to dynamically loaded forms */
+ $.fn.addCSRFTokenToForm = function() {
+ var token = getCSRFToken();
+ if (token) {
+ return $(this).each(function() {
+ if ($(this).data("csrf-token-checked") === "Y") return;
+ if (!isSameOrigin(location.href, $(this).attr("action"), true)) {
+ return $(this).data("csrf-token-checked", "Y");
+ }
+ $("").attr({ type: "hidden", name: "_rx_csrf_token", value: token }).appendTo($(this));
+ return $(this).data("csrf-token-checked", "Y");
+ });
+ } else {
+ return $(this);
+ }
+ };
+
/* Array for pending debug data */
window.rhymix_debug_pending_data = [];
@@ -154,6 +203,13 @@
/* jQuery(document).ready() */
jQuery(function($) {
+ /* CSRF token */
+ $("form[method]").filter(function() { return this.method.toUpperCase() == "POST"; }).addCSRFTokenToForm();
+ $(document).on("submit", "form[method='post']", $.fn.addCSRFTokenToForm);
+ $(document).on("focus", "input,select,textarea", function() {
+ $(this).parents("form[method]").filter(function() { return this.method.toUpperCase() == "POST"; }).addCSRFTokenToForm();
+ });
+
/* select - option의 disabled=disabled 속성을 IE에서도 체크하기 위한 함수 */
if(navigator.userAgent.match(/MSIE/)) {
$('select').each(function(i, sels) {
diff --git a/common/js/xml_handler.js b/common/js/xml_handler.js
index 9f65f0990..370aac865 100644
--- a/common/js/xml_handler.js
+++ b/common/js/xml_handler.js
@@ -28,6 +28,7 @@
params.module = module;
params.act = act;
params._rx_ajax_compat = 'XMLRPC';
+ params._rx_csrf_token = getCSRFToken();
// Fill in the XE vid.
if (typeof(xeVid) != "undefined") params.vid = xeVid;
@@ -46,9 +47,7 @@
}
// Check whether this is a cross-domain request. If so, use an alternative method.
- var _u1 = $("").attr("href", location.href)[0];
- var _u2 = $("").attr("href", url)[0];
- if (_u1.protocol != _u2.protocol || _u1.port != _u2.port) return send_by_form(url, params);
+ if (!isSameOrigin(location.href, url)) return send_by_form(url, params);
// Delay the waiting message for 1 second to prevent rapid blinking.
waiting_obj.css("opacity", 0.0);
@@ -172,6 +171,7 @@
params.module = action[0];
params.act = action[1];
params._rx_ajax_compat = 'JSON';
+ params._rx_csrf_token = getCSRFToken();
// Fill in the XE vid.
if (typeof(xeVid) != "undefined") params.vid = xeVid;
@@ -278,6 +278,7 @@
//if (action.length != 2) return;
params.module = action[0];
params.act = action[1];
+ params._rx_csrf_token = getCSRFToken();
// Fill in the XE vid.
if (typeof(xeVid) != "undefined") params.vid = xeVid;
diff --git a/common/tpl/common_layout.html b/common/tpl/common_layout.html
index 05b95716e..b59512931 100644
--- a/common/tpl/common_layout.html
+++ b/common/tpl/common_layout.html
@@ -10,6 +10,7 @@
+
{Context::getBrowserTitle()}
diff --git a/modules/editor/tpl/js/uploader.js b/modules/editor/tpl/js/uploader.js
old mode 100755
new mode 100644
index beb4807a0..eadbfe052
--- a/modules/editor/tpl/js/uploader.js
+++ b/modules/editor/tpl/js/uploader.js
@@ -54,7 +54,8 @@ var uploadAutosaveChecker = false;
mid : current_mid,
act : 'procFileUpload',
editor_sequence : seq,
- uploadTargetSrl : editorRelKeys[seq].primary.value
+ uploadTargetSrl : editorRelKeys[seq].primary.value,
+ _rx_csrf_token : getCSRFToken()
},
http_success : [302],
file_size_limit : Math.floor( (parseInt(cfg.allowedFileSize,10)||1024) / 1024 ),
diff --git a/tests/unit/framework/SecurityTest.php b/tests/unit/framework/SecurityTest.php
index adc5557b6..ca36d35a8 100644
--- a/tests/unit/framework/SecurityTest.php
+++ b/tests/unit/framework/SecurityTest.php
@@ -106,17 +106,21 @@ class SecurityTest extends \Codeception\TestCase\Test
public function testCheckCSRF()
{
+ $error_reporting = error_reporting(0);
+
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['HTTP_REFERER'] = '';
$this->assertTrue(Rhymix\Framework\Security::checkCSRF());
$_SERVER['REQUEST_METHOD'] = 'POST';
- $this->assertTrue(Rhymix\Framework\Security::checkCSRF());
+ $this->assertFalse(Rhymix\Framework\Security::checkCSRF());
$_SERVER['HTTP_REFERER'] = 'http://www.foobar.com/';
$this->assertFalse(Rhymix\Framework\Security::checkCSRF());
$this->assertTrue(Rhymix\Framework\Security::checkCSRF('http://www.rhymix.org/'));
+
+ error_reporting($error_reporting);
}
public function testCheckXEE()
diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php
index e263ef4ab..6f97c9cee 100644
--- a/tests/unit/framework/SessionTest.php
+++ b/tests/unit/framework/SessionTest.php
@@ -334,9 +334,16 @@ class SessionTest extends \Codeception\TestCase\Test
$this->assertFalse(Rhymix\Framework\Session::verifyToken($token2, '/wrong/key'));
$this->assertFalse(Rhymix\Framework\Session::verifyToken(strrev($token2)));
+ $token3 = Rhymix\Framework\Session::getGenericToken();
+ $this->assertEquals(16, strlen($token3));
+ $this->assertTrue(Rhymix\Framework\Session::verifyToken($token3));
+ $this->assertTrue(Rhymix\Framework\Session::verifyToken($token3, ''));
+ $this->assertFalse(Rhymix\Framework\Session::verifyToken($token3, '/wrong/key'));
+
Rhymix\Framework\Session::destroy();
$this->assertFalse(Rhymix\Framework\Session::verifyToken($token1));
$this->assertFalse(Rhymix\Framework\Session::verifyToken($token, '/my/key'));
+ $this->assertFalse(Rhymix\Framework\Session::getGenericToken());
}
public function testEncryption()