diff --git a/client/commonFramework/bindings.js b/client/commonFramework/bindings.js index 8f1aaa044..9eb8e74b1 100644 --- a/client/commonFramework/bindings.js +++ b/client/commonFramework/bindings.js @@ -21,6 +21,11 @@ window.common = (function(global) { }; common.init.push(function($) { + function stripTrailingHashes(url) { + let i = url.length - 1; + while (i >= 0 && url[i] === '#') i--; + return url.slice(0, i + 1); + } var $marginFix = $('.innerMarginFix'); $marginFix.css('min-height', $marginFix.height()); @@ -179,10 +184,9 @@ window.common = (function(global) { }); $('#search-issue').on('click', function() { - var queryIssue = window.location.href - .toString() - .split('?')[0] - .replace(/(#*)$/, ''); + var queryIssue = stripTrailingHashes( + window.location.href.toString().split('?')[0] + ); window.open( 'https://github.com/freecodecampchina/freecodecamp.cn/issues?q=' + 'is:issue is:all ' + @@ -196,4 +200,4 @@ window.common = (function(global) { }); return common; -}(window)); +}(window)); \ No newline at end of file diff --git a/client/commonFramework/init.js b/client/commonFramework/init.js index ff4130efd..6b8bcc15e 100644 --- a/client/commonFramework/init.js +++ b/client/commonFramework/init.js @@ -46,13 +46,13 @@ window.common = (function(global) { }; common.replaceFormActionAttr = function replaceFormAction(value) { - return value.replace(/]*>/, function(val) { + return value.replace(/]*>/, function(val) { return val.replace(/action(\s*?)=/, 'fccfaa$1='); }); }; common.replaceFccfaaAttr = function replaceFccfaaAttr(value) { - return value.replace(/]*>/, function(val) { + return value.replace(/]*>/, function(val) { return val.replace(/fccfaa(\s*?)=/, 'action$1='); }); }; diff --git a/package.json b/package.json index f1316f1c8..5ac660f6c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "lint-json": "npm run lint-server && npm run lint-challenges && npm run lint-resources && npm run lint-utils", "test-challenges": "babel-node seed/test-challenges.js | tap-spec", "pretest": "npm run lint", - "test": "npm run test-challenges" + "test": "npm run test-challenges", + "test-redos": "mocha test/commonFramework/bindings.test.js" }, "license": "(BSD-3-Clause AND CC-BY-SA-4.0)", "dependencies": { diff --git a/public/js/commonFramework-2c9795240d.js b/public/js/commonFramework-2c9795240d.js index 99b379a24..c9478682a 100644 --- a/public/js/commonFramework-2c9795240d.js +++ b/public/js/commonFramework-2c9795240d.js @@ -51,13 +51,13 @@ window.common = function(e) { } , a.replaceFormActionAttr = function(e) { - return e.replace(/]*>/, function(e) { + return e.replace(/]*>/, function(e) { return e.replace(/action(\s*?)=/, "fccfaa$1=") }) } , a.replaceFccfaaAttr = function(e) { - return e.replace(/]*>/, function(e) { + return e.replace(/]*>/, function(e) { return e.replace(/fccfaa(\s*?)=/, "action$1=") }) } diff --git a/test/commonFramework/bindings.test.js b/test/commonFramework/bindings.test.js new file mode 100644 index 000000000..b6d572408 --- /dev/null +++ b/test/commonFramework/bindings.test.js @@ -0,0 +1,45 @@ +// test/bindings.test.js +const { expect } = require('chai'); +const { performance } = require('perf_hooks'); + +// 安全替代函数 +function stripTrailingHashes(url) { + let i = url.length - 1; + while (i >= 0 && url[i] === '#') i--; + return url.slice(0, i + 1); +} + +// 模拟原版逻辑(用于对比测试) +function originalReplace(url) { + return url.replace(/#+$/, ''); +} + +describe('✅ stripTrailingHashes replacement test', function () { + this.timeout(10000); // 最多允许10秒运行 + + it('should match behavior of original regex for normal cases', () => { + const cases = [ + ["https://a.com/page#", "https://a.com/page"], + ["https://a.com/page###", "https://a.com/page"], + ["https://a.com/page#section", "https://a.com/page#section"], + ["https://a.com/page#section#", "https://a.com/page#section"], + ["https://a.com/page", "https://a.com/page"], + ]; + + for (const [input, expected] of cases) { + expect(stripTrailingHashes(input)).to.equal(expected); + expect(stripTrailingHashes(input)).to.equal(originalReplace(input)); + } + }); + + it('should not hang on malicious long string input', () => { + const attack = '#'.repeat(100000) + '@'; + const testUrl = 'https://a.com/' + attack; + const t0 = performance.now(); + const result = stripTrailingHashes(testUrl); + const t1 = performance.now(); + const duration = (t1 - t0) / 1000; + console.log(`⏱️ safeReplace() 执行耗时:${duration.toFixed(3)} s`); + expect(duration).to.be.lessThan(1); // 应该非常快(毫秒级) + }); +}); \ No newline at end of file