diff --git a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/dist/js/bootstrap.js index 170bd608f7fd..c6f9020438cf 100644 --- a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/dist/js/bootstrap.js +++ b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/dist/js/bootstrap.js @@ -511,7 +511,7 @@ if (typeof jQuery === 'undefined') { var $this = $(this) var href = $this.attr('href') if (href) { - href = href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + href = href && href.indexOf('#') !== -1 ? href.slice(href.lastIndexOf('#')) : href } var target = $this.attr('data-target') || href @@ -705,7 +705,7 @@ if (typeof jQuery === 'undefined') { function getTargetFromTrigger($trigger) { var href var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 + || ((href = $trigger.attr('href')) && (href.indexOf('#') !== -1 ? href.slice(href.lastIndexOf('#')) : href)) // strip for ie7 (safe) return $(document).find(target) } @@ -1265,7 +1265,8 @@ if (typeof jQuery === 'undefined') { var $this = $(this) var href = $this.attr('href') var target = $this.attr('data-target') || - (href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + //(href && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 + (href && href.replace(/^[^#]*(?=#\S+$)/, '')) // strip for ie7 - safe var $target = $(document).find(target) var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) diff --git a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/package.json b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/package.json new file mode 100644 index 000000000000..d5abde6da3a8 --- /dev/null +++ b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/package.json @@ -0,0 +1,8 @@ +{ + "name": "bootstrap-sample-tests", + "private": true, + "type": "commonjs", + "scripts": { + "test": "node --test ./test/carousel-href.spec.js ./test/collapse-href.spec.js" + } +} diff --git a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.spec.js b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.spec.js new file mode 100644 index 000000000000..db23515167c7 --- /dev/null +++ b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.spec.js @@ -0,0 +1,68 @@ +// Node built-in test – Carousel data-api href sanitize timing (fail if > LIMIT) +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const BOOTSTRAP_PATH = + process.env.BOOTSTRAP_PATH || + path.resolve(__dirname, '../dist/js/bootstrap.js'); + +const N = parseInt(process.env.LENGTH || '100000', 10); +const LIMIT = parseInt(process.env.LIMIT || '2000', 10); // 超过即失败 + +test('Carousel data-api href sanitize timing (fail if > LIMIT)', () => { + // 极简 jQuery stub:按事件名保存 handler + const handlers = Object.create(null); + + function wrap(raw) { + return { + on(event, selectorOrHandler, maybeHandler) { + const h = typeof maybeHandler === 'function' + ? maybeHandler + : (typeof selectorOrHandler === 'function' ? selectorOrHandler : null); + if (event && h) handlers[event] = h; + return this; // 链式 .on().on() + }, + find() { return { hasClass: () => false, data() { return {}; } }; }, + data() { return {}; }, + attr(name) { return raw && name === 'href' ? raw._href : null; }, + }; + } + function $(x) { return wrap(x); } + $.fn = { jquery: '3.4.1' }; + $.extend = Object.assign; + + // 满足 bootstrap 自检 + global.window = global; + global.document = {}; + global.jQuery = global.$ = $; + + // 载入本地 bootstrap(未修复/修复后的都可以) + require(BOOTSTRAP_PATH); + + // 精确拿到 Carousel 的 data-api 处理器 + const click = + handlers['click.bs.carousel.data-api'] || + handlers['click.bs.carousel']; + assert.equal(typeof click, 'function', 'failed to capture carousel handler'); + + const cases = [ + { name: 'nul', s: '\u0000'.repeat(N) + '\u0000' }, + { name: 'digits\\n@', s: '1'.repeat(N) + '\n@' }, + ]; + + let worst = 0; + for (const { name, s } of cases) { + const t0 = Date.now(); + try { click.call({ _href: s }, { preventDefault(){} }); } catch {} + const ms = Date.now() - t0; + worst = Math.max(worst, ms); + console.log(`[carousel] ${name.padEnd(10)} len=${s.length} -> ${ms} ms`); + } + console.log(`[carousel] worst = ${worst} ms (limit=${LIMIT})`); + + // 关键:超过 LIMIT 就判失败(红色 ✗) + if (worst > LIMIT) { + assert.fail(`too slow: ${worst}ms (> ${LIMIT}ms)`); + } +}); diff --git a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.test.js b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.test.js new file mode 100644 index 000000000000..84c324452652 --- /dev/null +++ b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/carousel-href.test.js @@ -0,0 +1,62 @@ +// Node built-in test – Carousel data-api href sanitize timing (pass if < LIMIT) +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const BOOTSTRAP_PATH = + process.env.BOOTSTRAP_PATH || + path.resolve(__dirname, '../dist/js/bootstrap.js'); + +const N = parseInt(process.env.LENGTH || '100000', 10); +const LIMIT = parseInt(process.env.LIMIT || '2000', 10); + +test('Carousel data-api href sanitize timing (pass if < LIMIT)', () => { + const handlers = Object.create(null); + + function wrap(raw) { + return { + on(event, selectorOrHandler, maybeHandler) { + const h = typeof maybeHandler === 'function' + ? maybeHandler + : (typeof selectorOrHandler === 'function' ? selectorOrHandler : null); + if (event && h) handlers[event] = h; + return this; + }, + find() { return { hasClass: () => false, data() { return {}; } }; }, + data() { return {}; }, + attr(name) { return raw && name === 'href' ? raw._href : null; }, + }; + } + function $(x) { return wrap(x); } + $.fn = { jquery: '3.4.1' }; + $.extend = Object.assign; + + global.window = global; + global.document = {}; + global.jQuery = global.$ = $; + + require(BOOTSTRAP_PATH); + + const click = + handlers['click.bs.carousel.data-api'] || + handlers['click.bs.carousel']; + assert.equal(typeof click, 'function', 'failed to capture carousel handler'); + + const cases = [ + { name: 'nul', s: '\u0000'.repeat(N) + '\u0000' }, + { name: 'digits\\n@', s: '1'.repeat(N) + '\n@' }, + ]; + + let worst = 0; + for (const { name, s } of cases) { + const t0 = Date.now(); + try { click.call({ _href: s }, { preventDefault(){} }); } catch {} + const ms = Date.now() - t0; + worst = Math.max(worst, ms); + console.log(`[carousel] ${name.padEnd(10)} len=${s.length} -> ${ms} ms`); + } + console.log(`[carousel] worst = ${worst} ms (limit=${LIMIT})`); + + // 修复后应当 < LIMIT;否则失败 + assert.ok(worst < LIMIT, `too slow: ${worst}ms (>= ${LIMIT}ms)`); +}); diff --git a/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/collapse-href.spec.js b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/collapse-href.spec.js new file mode 100644 index 000000000000..c210cd22e1ce --- /dev/null +++ b/src/Security/samples/ClaimsTransformation/wwwroot/lib/bootstrap/test/collapse-href.spec.js @@ -0,0 +1,65 @@ +// Node built-in test – Collapse data-api href sanitize timing (fail if > LIMIT ms) +const test = require('node:test'); +const assert = require('node:assert/strict'); +const path = require('node:path'); + +const BOOTSTRAP_PATH = + process.env.BOOTSTRAP_PATH || + path.resolve(__dirname, '../dist/js/bootstrap.js'); + +const N = parseInt(process.env.LENGTH || '100000', 10); +const LIMIT = parseInt(process.env.LIMIT || '2000', 10); // 超过就失败 + +test('Collapse data-api href sanitize timing', () => { + // 极简 jQuery stub:按事件名存 handler + const handlers = Object.create(null); + + function wrap(raw) { + return { + on(event, selectorOrHandler, maybeHandler) { + const h = typeof maybeHandler === 'function' + ? maybeHandler + : (typeof selectorOrHandler === 'function' ? selectorOrHandler : null); + if (event && h) handlers[event] = h; + return this; + }, + find() { return { hasClass: () => false, data() { return {}; } }; }, + data() { return {}; }, + attr(name) { return raw && name === 'href' ? raw._href : null; }, + }; + } + function $(x) { return wrap(x); } + $.fn = { jquery: '3.4.1' }; + $.extend = Object.assign; + + global.window = global; + global.document = {}; + global.jQuery = global.$ = $; + + require(BOOTSTRAP_PATH); + + const click = + handlers['click.bs.collapse.data-api'] || + handlers['click.bs.collapse']; + assert.equal(typeof click, 'function', 'failed to capture collapse handler'); + + const cases = [ + { name: 'nul', s: '\u0000'.repeat(N) + '\u0000' }, + { name: 'digits\\n@', s: '1'.repeat(N) + '\n@' }, + ]; + + let worst = 0; + for (const { name, s } of cases) { + const t0 = Date.now(); + try { click.call({ _href: s }, { preventDefault(){} }); } catch {} + const ms = Date.now() - t0; + worst = Math.max(worst, ms); + console.log(`[collapse] ${name.padEnd(10)} len=${s.length} -> ${ms} ms`); + } + console.log(`[collapse] worst = ${worst} ms (limit=${LIMIT})`); + + // 关键:超过 LIMIT 就判失败(红色 ✗) + if (worst > LIMIT) { + assert.fail(`too slow: ${worst}ms (> ${LIMIT}ms)`); + } +});