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()