diff --git a/CHANGELOG.md b/CHANGELOG.md index 7765382..9943d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [14.1.1] - 2026-01-11 +### Security +- Fixed regression from v13 in linkify inline rule. Specific patterns could + cause high CPU use. Thanks to @ltduc147 for report. + ## [14.1.0] - 2024-03-19 ### Changed @@ -648,6 +653,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Renamed presets folder (configs -> presets). +[14.1.1]: https://github.com/markdown-it/markdown-it/compare/14.1.0...14.1.1 [14.1.0]: https://github.com/markdown-it/markdown-it/compare/14.0.0...14.1.0 [14.0.0]: https://github.com/markdown-it/markdown-it/compare/13.0.2...14.0.0 [13.0.2]: https://github.com/markdown-it/markdown-it/compare/13.0.1...13.0.2 diff --git a/lib/rules_inline/linkify.mjs b/lib/rules_inline/linkify.mjs index fdc817a..3daf1d5 100644 --- a/lib/rules_inline/linkify.mjs +++ b/lib/rules_inline/linkify.mjs @@ -30,7 +30,14 @@ export default function linkify (state, silent) { if (url.length <= proto.length) return false // disallow '*' at the end of the link (conflicts with emphasis) - url = url.replace(/\*+$/, '') + // do manual backsearch to avoid perf issues with regex /\*+$/ on "****...****a". + let urlEnd = url.length + while (urlEnd > 0 && url.charCodeAt(urlEnd - 1) === 0x2A/* * */) { + urlEnd-- + } + if (urlEnd !== url.length) { + url = url.slice(0, urlEnd) + } const fullUrl = state.md.normalizeLink(url) if (!state.md.validateLink(fullUrl)) return false diff --git a/test/pathological.mjs b/test/pathological.mjs index c2db10f..e510e3b 100644 --- a/test/pathological.mjs +++ b/test/pathological.mjs @@ -4,7 +4,7 @@ import crypto from 'node:crypto' import { Worker as JestWorker } from 'jest-worker' import { readFileSync } from 'fs' -async function test_pattern (str) { +async function test_pattern (str, mdOpts) { const worker = new JestWorker( new URL('./pathological_worker.js', import.meta.url), { @@ -18,7 +18,7 @@ async function test_pattern (str) { try { result = await Promise.race([ - worker.render(str), + worker.render(str, mdOpts), new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Terminated (timeout exceeded)')), 3000).unref() }) @@ -163,5 +163,12 @@ describe('Pathological sequences speed', () => { it('hardbreak whitespaces pattern', async () => { await test_pattern('x' + ' '.repeat(150000) + 'x \nx') }) + + it('linkify-it wrapper trailing asterisks pattern', async () => { + await test_pattern( + 'https://test.com?' + '*'.repeat(70000) + 'a', + { linkify: true } + ) + }) }) }) diff --git a/test/pathological_worker.js b/test/pathological_worker.js index 76259c8..d0b0cc6 100644 --- a/test/pathological_worker.js +++ b/test/pathological_worker.js @@ -1,5 +1,5 @@ 'use strict' -exports.render = async (str) => { - return (await import('../index.mjs')).default().render(str) +exports.render = async (str, mdOpts) => { + return (await import('../index.mjs')).default(mdOpts).render(str) }