Skip to content

Commit f043d72

Browse files
committed
fix(security): clear CodeQL alerts — complete tag stripping, escape widget DOM
SubtitlesProcessor: tag strip now removes residual angle brackets (complete multi-character sanitization). Widget: catalogue fields (code/authority/name/scripts/language) escaped before innerHTML interpolation.
1 parent ce8887c commit f043d72

2 files changed

Lines changed: 28 additions & 20 deletions

File tree

src/components/SubtitlesProcessor.vue

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* Preserves subtitle structure (timestamps, indices). Only the dialogue
88
* text gets transliterated.
99
*/
10-
import { ref, computed, onMounted, onUnmounted } from "vue"
10+
import { ref, onMounted, onUnmounted } from "vue"
1111
import { createWorkerClient, type WorkerClient } from "../scripts/worker-client"
1212
1313
interface Props {
@@ -38,7 +38,8 @@ async function ensureEngine() {
3838
3939
// Regex: capture subtitle cues with timestamps.
4040
// SRT/VTT pattern: index (optional), time range line, dialogue.
41-
const CUE_RE = /(\d+\s*\n)?((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}\s*-->\s*(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})\s*\n([\s\S]*?)(?=\n\s*\n|\n\d+\s*\n|\n(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}|$)/g
41+
const CUE_RE =
42+
/(\d+\s*\n)?((?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}\s*-->\s*(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3})\s*\n([\s\S]*?)(?=\n\s*\n|\n\d+\s*\n|\n(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}|$)/g
4243
4344
async function run() {
4445
if (!client) return
@@ -49,7 +50,8 @@ async function run() {
4950
try {
5051
const out = inputText.value.replace(CUE_RE, async (_match, idx, time, dialogue: string) => {
5152
// Strip HTML formatting tags before transliterating
52-
const stripped = dialogue.replace(/<[^>]+>/g, "").trim()
53+
const stripped = dialogue.replace(/<[^>]*>/g, "").replace(/[<>]/g, "").trim()
54+
// eslint-disable-next-line no-control-regex -- \x00-\x7F is the full ASCII range
5355
if (!/[^\x00-\x7F]/.test(stripped)) {
5456
return `${idx ?? ""}${time}\n${dialogue}`.trim()
5557
}
@@ -84,7 +86,8 @@ async function run() {
8486
8587
const outParts: string[] = []
8688
for (const cue of cues) {
87-
const stripped = cue.dialogue.replace(/<[^>]+>/g, "").trim()
89+
const stripped = cue.dialogue.replace(/<[^>]*>/g, "").replace(/[<>]/g, "").trim()
90+
// eslint-disable-next-line no-control-regex -- \x00-\x7F is the full ASCII range
8891
if (!/[^\x00-\x7F]/.test(stripped)) {
8992
outParts.push(`${cue.idx}${cue.time}\n${cue.dialogue}`.trim())
9093
continue
@@ -93,7 +96,7 @@ async function run() {
9396
const transliterated = await client.transliterate(system.value, stripped)
9497
count++
9598
outParts.push(`${cue.idx}${cue.time}\n${transliterated}`)
96-
} catch (e) {
99+
} catch {
97100
outParts.push(`${cue.idx}${cue.time}\n${cue.dialogue}`)
98101
}
99102
}
@@ -141,11 +144,13 @@ onUnmounted(() => client?.terminate())
141144
<span class="pane-label">Transliterated output</span>
142145
<button v-if="output" class="copy-btn" @click="copyOutput">Copy</button>
143146
</header>
144-
<pre>{{ output || 'Output appears here.' }}</pre>
147+
<pre>{{ output || "Output appears here." }}</pre>
145148
</div>
146149
</div>
147150

148-
<p v-if="cueCount > 0" class="cue-count tnum">{{ cueCount }} cue{{ cueCount > 1 ? "s" : "" }} transliterated.</p>
151+
<p v-if="cueCount > 0" class="cue-count tnum">
152+
{{ cueCount }} cue{{ cueCount > 1 ? "s" : "" }} transliterated.
153+
</p>
149154
<p v-if="error" class="error">⚠ {{ error }}</p>
150155
<p class="privacy">Text never leaves your browser.</p>
151156
</div>
@@ -185,7 +190,9 @@ onUnmounted(() => client?.terminate())
185190
color: var(--color-ink);
186191
border-radius: 1px;
187192
}
188-
.control-field select:focus { border-color: var(--color-brand); }
193+
.control-field select:focus {
194+
border-color: var(--color-brand);
195+
}
189196
190197
.run-btn {
191198
font-family: var(--font-mono);
@@ -203,15 +210,19 @@ onUnmounted(() => client?.terminate())
203210
background: var(--color-highlight-deep);
204211
border-color: var(--color-highlight-deep);
205212
}
206-
.run-btn:disabled { opacity: 0.5; }
213+
.run-btn:disabled {
214+
opacity: 0.5;
215+
}
207216
208217
.subs-grid {
209218
display: grid;
210219
grid-template-columns: 1fr;
211220
gap: 1rem;
212221
}
213222
@media (min-width: 900px) {
214-
.subs-grid { grid-template-columns: 1fr 1fr; }
223+
.subs-grid {
224+
grid-template-columns: 1fr 1fr;
225+
}
215226
}
216227
217228
.io-pane {
@@ -240,7 +251,8 @@ onUnmounted(() => client?.terminate())
240251
color: var(--color-stone-light);
241252
}
242253
243-
textarea, pre {
254+
textarea,
255+
pre {
244256
flex: 1;
245257
min-height: 320px;
246258
font-family: var(--font-mono);

src/scripts/widget.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,7 @@
99
* no full catalogue bundle, no main-thread jank.
1010
*/
1111

12-
import {
13-
configure,
14-
reset,
15-
transliterateAsync,
16-
} from "interscript-ts"
12+
import { configure, reset, transliterateAsync } from "interscript-ts"
1713
import { mapStrategies } from "./map-strategies"
1814

1915
// Configure once with HTTP loader + persistent cache.
@@ -84,7 +80,7 @@ class InterscriptWidget extends HTMLElement {
8480
<style>
8581
:host {
8682
display: block;
87-
font-family: "Inter Tight", system-ui, -apple-system, sans-serif;
83+
font-family: "Inter Tight Variable", "Inter Tight", system-ui, sans-serif;
8884
background: #fcfaf4;
8985
color: #1a1d1f;
9086
padding: 1.25rem;
@@ -195,18 +191,18 @@ class InterscriptWidget extends HTMLElement {
195191
${this.systems
196192
.map(
197193
(s) =>
198-
`<option value="${s.code}" ${s.code === this.selected ? "selected" : ""}>${s.authority.toUpperCase()} · ${s.name}</option>`,
194+
`<option value="${this.escapeHtml(s.code)}" ${s.code === this.selected ? "selected" : ""}>${this.escapeHtml(s.authority.toUpperCase())} · ${this.escapeHtml(s.name)}</option>`,
199195
)
200196
.join("")}
201197
</select>
202198
</div>
203199
<div class="pair">
204200
<div class="field">
205-
<label>${selected?.sourceScript ?? "Source"} ${selected?.language ? ${selected.language}` : ""}</label>
201+
<label>${this.escapeHtml(selected?.sourceScript ?? "Source")} ${selected?.language ? ${this.escapeHtml(selected.language)}` : ""}</label>
206202
<textarea rows="3" placeholder="Type here…">${this.escapeHtml(this.input)}</textarea>
207203
</div>
208204
<div class="field">
209-
<label>${selected?.destinationScript ?? "Latin"}</label>
205+
<label>${this.escapeHtml(selected?.destinationScript ?? "Latin")}</label>
210206
<div class="output ${this.loading ? "loading" : ""} ${this.error ? "error" : ""}">
211207
${this.error ? "⚠ " + this.escapeHtml(this.error) : this.loading ? "Loading…" : this.escapeHtml(this.output)}
212208
</div>

0 commit comments

Comments
 (0)