Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/petite-parts-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bnidev/js-utils": minor
---

feat(string): `stripHtmlTags` now supports an optional `maxLength` parameter to mitigate regex denial-of-service (ReDoS) risks
16 changes: 16 additions & 0 deletions src/string/__tests__/stripHtmlTags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@ describe('stripHtmlTags', () => {
'This is bold and italic text.'
)
})

it('throws if input exceeds default maxLength of 1000', () => {
const longInput = `<p>${'x'.repeat(1001)}</p>`
expect(() => stripHtmlTags(longInput)).toThrow('Input too long')
})

it('does not throw if input is exactly at default maxLength', () => {
const validInput = `<p>${'x'.repeat(993)}</p>` // Total 1000 chars
expect(() => stripHtmlTags(validInput)).not.toThrow()
})

it('respects custom maxLength argument', () => {
const input = `<span>${'a'.repeat(5000)}</span>`
expect(() => stripHtmlTags(input, 6000)).not.toThrow()
expect(() => stripHtmlTags(input, 1000)).toThrow('Input too long')
})
})
31 changes: 25 additions & 6 deletions src/string/stripHtmlTags.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
/**
* Removes all HTML tags from a string, returning plain text.
*
* @param html - The input string containing HTML.
* @returns The string without HTML tags.
* Applies the tag-stripping regular expression in a loop to handle nested or malformed tags safely. To mitigate potential performance risks from ambiguous regular expressions (e.g. catastrophic backtracking), the function enforces a maximum input length.
*
* @param html - The input string that may contain HTML.
* @param maxLength - Maximum allowed input length. Defaults to 1000 characters.
* Throws an error if the input exceeds this limit.
*
* @returns The plain text string with all HTML tags removed.
*
* @throws If the input exceeds the maximum allowed length.
*
* @category String
*
Expand All @@ -17,11 +24,23 @@
*
* @example Usage
* ```ts
* stripHtml('<p>Hello <strong>World</strong></p>') // → 'Hello World'
* stripHtmlTags('<p>Hello <strong>World</strong></p>')
* // → 'Hello World'
* ```
*/
export function stripHtmlTags(html: string): string {
export function stripHtmlTags(html: string, maxLength = 1000): string {
if (!html) return ''
// Simple regex to remove anything between < and >
return html.replace(/<[^>]*>/g, '')
if (html.length > maxLength) {
throw new Error(`Input too long (max ${maxLength} characters)`)
}

let prev: string
let current = html

do {
prev = current
current = current.replace(/<[^<>]*>/g, '')
} while (current !== prev)

return current
}