From 71484e72057cb97004611322dd4968bce9e6a909 Mon Sep 17 00:00:00 2001 From: cecechen04 Date: Mon, 26 May 2025 14:19:52 +0800 Subject: [PATCH 1/3] fix: eliminate ReDoS in bindings.js, add regression test, and script for test-redos --- client/commonFramework/bindings.js | 18 ++++++++--- package.json | 3 +- test/commonFramework/bindings.test.js | 45 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 test/commonFramework/bindings.test.js diff --git a/client/commonFramework/bindings.js b/client/commonFramework/bindings.js index 8f1aaa044..33d6bb7a8 100644 --- a/client/commonFramework/bindings.js +++ b/client/commonFramework/bindings.js @@ -21,6 +21,15 @@ window.common = (function(global) { }; common.init.push(function($) { + /** + * 安全替代 `.replace(/#+$/, '')`,用于去除 URL 或字符串末尾多余的 # + * 线性处理,无正则回溯风险,适用于高安全要求场景 + */ + 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 +188,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 +204,4 @@ window.common = (function(global) { }); return common; -}(window)); +}(window)); \ No newline at end of file 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/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 From 792d4c4fd4ad8b5b71319593ef8e99d65be1f161 Mon Sep 17 00:00:00 2001 From: cecechen04 Date: Tue, 27 May 2025 15:24:27 +0800 Subject: [PATCH 2/3] fix: eliminate ReDoS in bindings.js and add regression test --- client/commonFramework/bindings.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/commonFramework/bindings.js b/client/commonFramework/bindings.js index 33d6bb7a8..9eb8e74b1 100644 --- a/client/commonFramework/bindings.js +++ b/client/commonFramework/bindings.js @@ -21,10 +21,6 @@ window.common = (function(global) { }; common.init.push(function($) { - /** - * 安全替代 `.replace(/#+$/, '')`,用于去除 URL 或字符串末尾多余的 # - * 线性处理,无正则回溯风险,适用于高安全要求场景 - */ function stripTrailingHashes(url) { let i = url.length - 1; while (i >= 0 && url[i] === '#') i--; From 693bbfc77201c80f719e799708d025a9619e4ab4 Mon Sep 17 00:00:00 2001 From: cecechen04 Date: Wed, 18 Jun 2025 01:22:49 +0800 Subject: [PATCH 3/3] fix: patch ReDoS vulnerability in code-uri.js --- client/commonFramework/code-uri.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/commonFramework/code-uri.js b/client/commonFramework/code-uri.js index fefe45e0f..e378fe3b0 100644 --- a/client/commonFramework/code-uri.js +++ b/client/commonFramework/code-uri.js @@ -110,7 +110,10 @@ window.common = (function(global) { if (history && typeof history.replaceState === 'function') { // grab the url up to the query // destroy any hash symbols still clinging to life - const url = (location.href.split('?')[0]).replace(/(#*)$/, ''); + const href = location.href.split('?')[0]; + let i = href.length - 1; + while (i >= 0 && href[i] === '#') i--; + const url = href.slice(0, i + 1); history.replaceState( history.state, null,