Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit a840b07

Browse files
committed
fix(blog): loop HTML tag stripping to prevent incomplete sanitization
The single-pass .replace(/<[^>]+>/g, "") in calculateReadingTime() was flagged by CodeQL as vulnerable to incomplete multi-character sanitization. Input like "<scr<script>ipt>" would still contain "<script" after one pass. Added a stripHtmlTags() helper that loops the replacement until stable, plus a final pass to remove any remaining angle brackets.
1 parent b4edf8d commit a840b07

1 file changed

Lines changed: 34 additions & 17 deletions

File tree

  • apps/web-roo-code/src/lib/blog

apps/web-roo-code/src/lib/blog/time.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,23 @@ export function formatPostDatePt(publishDate: string): string {
9797
return publishDate
9898
}
9999

100+
/**
101+
* Repeatedly strip HTML tags until no more remain.
102+
* A single-pass replacement is vulnerable to incomplete sanitization when
103+
* the input contains nested/split patterns like `<scr<script>ipt>`.
104+
*/
105+
function stripHtmlTags(text: string): string {
106+
const TAG_RE = /<[^>]+>/g
107+
let previous = text
108+
let result = text.replace(TAG_RE, "")
109+
while (result !== previous) {
110+
previous = result
111+
result = result.replace(TAG_RE, "")
112+
}
113+
// Final safety: remove any remaining angle brackets
114+
return result.replace(/[<>]/g, "")
115+
}
116+
100117
/**
101118
* Calculate reading time for a piece of content
102119
* Uses average reading speed of 200 words per minute
@@ -105,23 +122,23 @@ export function formatPostDatePt(publishDate: string): string {
105122
*/
106123
export function calculateReadingTime(content: string): number {
107124
// Strip markdown syntax for more accurate word count
108-
const plainText = content
109-
// Remove code blocks
110-
.replace(/```[\s\S]*?```/g, "")
111-
// Remove inline code
112-
.replace(/`[^`]+`/g, "")
113-
// Remove images
114-
.replace(/!\[.*?\]\(.*?\)/g, "")
115-
// Remove links but keep text
116-
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
117-
// Remove headers markers
118-
.replace(/^#{1,6}\s+/gm, "")
119-
// Remove emphasis
120-
.replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, "$1")
121-
// Remove horizontal rules
122-
.replace(/^[-*_]{3,}\s*$/gm, "")
123-
// Remove HTML tags
124-
.replace(/<[^>]+>/g, "")
125+
const plainText = stripHtmlTags(
126+
content
127+
// Remove code blocks
128+
.replace(/```[\s\S]*?```/g, "")
129+
// Remove inline code
130+
.replace(/`[^`]+`/g, "")
131+
// Remove images
132+
.replace(/!\[.*?\]\(.*?\)/g, "")
133+
// Remove links but keep text
134+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
135+
// Remove headers markers
136+
.replace(/^#{1,6}\s+/gm, "")
137+
// Remove emphasis
138+
.replace(/[*_]{1,2}([^*_]+)[*_]{1,2}/g, "$1")
139+
// Remove horizontal rules
140+
.replace(/^[-*_]{3,}\s*$/gm, ""),
141+
)
125142

126143
// Count words (split on whitespace)
127144
const words = plainText

0 commit comments

Comments
 (0)