From e0ce108e0806d18353ad85125b2b5f1b1c67e07d Mon Sep 17 00:00:00 2001 From: Osmium Sorcerer Date: Sat, 6 Jun 2026 02:07:05 +0000 Subject: CSP hardening: remove inline scripts The next layer after input validaton to achive the paranoid levels of security. Remove all event handlers inside HTML attributes and add them in TS for each element, allowing `script-src 'self'` to be used as a CSP directive. Buttons that passed some value and had a shared function went into a global listener with data-action attribute, while all the individual elements received their own event listener. This is a mess, but my goal was to end up as close as I could to one-to-one translation of how functions were originally attached to elements. --- public/client.html | 126 ++++++++------------- webAO/client.ts | 1 + webAO/dom-events.ts | 226 +++++++++++++++++++++++++++++++++++++ webAO/packets/handlers/handleSI.ts | 6 +- 4 files changed, 276 insertions(+), 83 deletions(-) create mode 100644 webAO/dom-events.ts diff --git a/public/client.html b/public/client.html index 4fa3216..55214d3 100644 --- a/public/client.html +++ b/public/client.html @@ -106,12 +106,11 @@

Choose your character

- +



@@ -124,7 +123,7 @@

Error code: (none)

-
@@ -139,57 +138,57 @@
- - - - - + + + + +
- +
- +
- +
- +
- +
- +
- +
- +
- +
@@ -202,13 +201,13 @@ id="client_evi" src="" alt="Character Evidence" - onerror="imgError(this);" + data-error="img" /> Testimony overlay -
-
@@ -268,7 +264,8 @@ id="button_1" alt="Hold it!" class="client_button" - onclick="toggleShout(1)" + data-action="toggle-shout" + data-id="1" > Hold it! @@ -276,7 +273,8 @@ id="button_2" alt="Objection!" class="client_button" - onclick="toggleShout(2)" + data-action="toggle-shout" + data-id="2" > Objection! @@ -284,7 +282,8 @@ id="button_3" alt="Take That!" class="client_button" - onclick="toggleShout(3)" + data-action="toggle-shout" + data-id="3" > Take That! @@ -292,8 +291,9 @@ id="button_4" alt="Custom" class="client_button" - onclick="toggleShout(4)" style="display: none" + data-action="toggle-shout" + data-id="4" > Custom @@ -328,8 +328,8 @@ id="button_flip" alt="Flip" class="client_button" - onclick="toggleEffect(this)" style="display: none" + data-action="toggle-effect" > Flip @@ -337,7 +337,7 @@ id="button_flash" alt="Flash" class="client_button" - onclick="toggleEffect(this)" + data-action="toggle-effect" > Flash @@ -345,8 +345,8 @@ id="button_shake" alt="Shake" class="client_button" - onclick="toggleEffect(this)" style="display: none" + data-action="toggle-effect" > Shake @@ -358,7 +358,6 @@
@@ -579,23 +568,23 @@
Main Menu
- + 📍 - + 💼 - + 🔧 - + - + 🚨 @@ -615,7 +604,7 @@ id="bg_preview" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" alt="Background Preview" - onerror="imgError(this);" + data-error="img" /> @@ -624,7 +613,6 @@
@@ -640,7 +628,6 @@ id="bg_change" alt="Change" class="client_button hover_button" - onclick="changeBackgroundOOC()" > Change @@ -686,7 +673,7 @@ class="evi_icon" src="" alt="Evidence Icon" - onerror="imgError(this);" + data-error="img" />
Add @@ -733,7 +718,6 @@ id="evi_edit" alt="Edit Evidence" class="client_button hover_button inactive" - onclick="editEvidence()" > Edit @@ -741,7 +725,6 @@ id="evi_cancel" alt="Cancel Evidence" class="client_button hover_button inactive" - onclick="cancelEvidence()" > Cancel @@ -749,7 +732,6 @@ id="evi_del" alt="Remove Evidence" class="client_button hover_button inactive" - onclick="deleteEvidence()" > Remove @@ -762,7 +744,7 @@ id="button_present" alt="Present" class="client_button" - onclick="toggleEffect(this)" + data-action="toggle-effect" > Present @@ -781,30 +763,26 @@ max="1" value="1" step="0.01" - onchange="changeMusicVolume()" />

SFX

Shouts

Testimony/Guilty

@@ -817,7 +795,6 @@ max="1" value="1" step="0.01" - onchange="changeBlipVolume()" />

@@ -825,7 +802,6 @@ @@ -883,10 +858,9 @@ - +

Pan-tilt (experimental)

@@ -895,7 +869,6 @@ type="checkbox" id="client_pantilt" name="client_pantilt" - onclick="switchPanTilt()" />

@@ -905,7 +878,6 @@ type="checkbox" id="client_hdviewport" name="client_hdviewport" - onclick="switchAspectRatio()" />
@@ -913,7 +885,6 @@ type="checkbox" id="client_hdviewport_offset" id="client_hdviewport_offset" - onclick="switchChatOffset()" />

@@ -925,7 +896,6 @@ rows="4" cols="10" placeholder="Put 1 callword per line here" - onchange="changeCallwords()" >

@@ -941,7 +911,7 @@

- @@ -993,7 +963,6 @@ diff --git a/webAO/client.ts b/webAO/client.ts index f013f0a..7ad885f 100644 --- a/webAO/client.ts +++ b/webAO/client.ts @@ -23,6 +23,7 @@ import { } from "./client/fetchLists"; import { ExMsgType, handleRegisterCredential, handleAssertCredential, AuthState } from "./ext_packet" import { initWebAuthn } from "./auth" +import "./dom-events" const { ip: serverIP, connect, mode, theme, serverName, char: autoChar, area: autoArea } = queryParser(); export { autoChar, autoArea }; diff --git a/webAO/dom-events.ts b/webAO/dom-events.ts new file mode 100644 index 0000000..dbb0002 --- /dev/null +++ b/webAO/dom-events.ts @@ -0,0 +1,226 @@ +import { sendPasskeyLoginRequest } from "./ext_packet"; +import { pickChar } from "./dom/pickChar"; +import { ReconnectButton } from "./dom/reconnectButton"; +import { toggleShout } from "./dom/toggleShout"; +import { toggleEffect } from "./dom/toggleEffect"; +import { changeCharacter } from "./dom/changeCharacter"; +import { randomCharacterOOC } from "./dom/randomCharacterOOC"; +import { toggleElement } from "./dom/toggleElement"; +import { resetOffset } from "./dom/resetOffset"; +import { showname_click } from "./dom/showNameClick"; +import { initWT } from "./dom/initWT"; +import { initCE } from "./dom/initCE"; +import { notguilty } from "./dom/notGuilty"; +import { guilty } from "./dom/guilty"; +import { redHPD } from "./dom/redHPD"; +import { addHPD } from "./dom/addHPD"; +import { redHPP } from "./dom/redHPP"; +import { addHPP } from "./dom/addHPP"; +import { toggleMenu } from "./dom/toggleMenu"; +import { callMod } from "./dom/callMod"; +import { changeBackgroundOOC } from "./dom/changeBackgroundOOC"; +import { addEvidence } from "./dom/addEvidence"; +import { editEvidence } from "./dom/editEvidence"; +import { cancelEvidence } from "./dom/cancelEvidence"; +import { deleteEvidence } from "./dom/deleteEvidence"; +import { iniedit } from "./dom/iniEdit"; +import { switchPanTilt } from "./dom/switchPanTilt"; +import { switchAspectRatio } from "./dom/switchAspectRatio"; +import { switchChatOffset } from "./dom/switchChatOffset"; +import { DisconnectButton } from "./dom/disconnectButton"; +import { onReplayGo } from "./dom/onReplayGo"; +import { chartable_filter } from "./dom/charTableFilter"; +import { musiclist_filter } from "./dom/musicListFilter"; +import { musiclist_click } from "./dom/musicListClick"; +import { changeRoleOOC } from "./dom/changeRoleOOC"; +import { mutelist_click } from "./dom/muteListClick"; +import { updateBackgroundPreview } from "./dom/updateBackgroundPreview"; +import { updateEvidenceIcon } from "./dom/updateEvidenceIcon"; +import { changeMusicVolume } from "./dom/changeMusicVolume"; +import { changeBlipVolume } from "./dom/changeBlipVolume"; +import { setChatbox } from "./dom/setChatbox"; +import { reloadTheme } from "./dom/reloadTheme"; +import { updateIniswap } from "./dom/updateIniswap"; +import { changeCallwords } from "./dom/changeCallwords"; +import { imgError } from "./dom/imgError"; +import { charError } from "./dom/charError"; +import { opusCheck } from "./dom/opusCheck"; +import { onEnter } from "./dom/onEnter"; +import { onOOCEnter } from "./dom/onOOCEnter"; +import { + changeSFXVolume, + changeShoutVolume, + changeTestimonyVolume, +} from "./dom/changeVolume"; + +document.addEventListener("click", (e: MouseEvent) => { + if (!(e.target instanceof HTMLElement)) { + return; + } + const button = e.target.closest("[data-action]"); + if (!(button instanceof HTMLElement)) { + return; + } + + switch (button.dataset.action) { + case "pick-char": + pickChar(Number(button.dataset.id)); + break; + case "toggle-shout": + toggleShout(Number(button.dataset.id)); + break; + case "toggle-effect": + toggleEffect(button); + break; + case "toggle-menu": + toggleMenu(Number(button.dataset.id)); + break; + default: + break; + } +}); + +document + .getElementById("client_reconnect") + .addEventListener("click", () => ReconnectButton()); +document + .getElementById("char_change") + .addEventListener("click", changeCharacter); +document + .getElementById("char_random") + .addEventListener("click", () => randomCharacterOOC()); +document + .getElementById("button_toggle_pairing") + .addEventListener("click", () => toggleElement("pairing_settings")); +document.getElementById("pair_reset").addEventListener("click", resetOffset); +document.getElementById("showname").addEventListener("click", showname_click); +document.getElementById("menu_wt").addEventListener("click", () => initWT()); +document.getElementById("menu_ce").addEventListener("click", () => initCE()); +document + .getElementById("menu_nguilty") + .addEventListener("click", () => notguilty()); +document + .getElementById("menu_guilty") + .addEventListener("click", () => guilty()); +document.getElementById("menu_rhpd").addEventListener("click", () => redHPD()); +document.getElementById("menu_ahpd").addEventListener("click", () => addHPD()); +document.getElementById("menu_rhpp").addEventListener("click", () => redHPP()); +document.getElementById("menu_ahpp").addEventListener("click", () => addHPP()); +document.getElementById("menu_cm").addEventListener("click", () => callMod()); +document + .getElementById("bg_change") + .addEventListener("click", () => changeBackgroundOOC()); +document + .getElementById("evi_add") + .addEventListener("click", () => addEvidence()); +document + .getElementById("evi_edit") + .addEventListener("click", () => editEvidence()); +document + .getElementById("evi_cancel") + .addEventListener("click", () => cancelEvidence()); +document + .getElementById("evi_del") + .addEventListener("click", () => deleteEvidence()); +document + .getElementById("client_inichange") + .addEventListener("click", () => iniedit()); +document + .getElementById("client_pantilt") + .addEventListener("click", () => switchPanTilt()); +document + .getElementById("client_hdviewport") + .addEventListener("click", () => switchAspectRatio()); +document + .getElementById("client_hdviewport_offset") + .addEventListener("click", () => switchChatOffset()); +document + .getElementById("client_disconnect") + .addEventListener("click", () => DisconnectButton()); +document + .getElementById("client_replaygo") + .addEventListener("click", onReplayGo); + +document + .getElementById("client_charactersearch") + .addEventListener("input", chartable_filter); +document + .getElementById("client_musicsearch") + .addEventListener("input", musiclist_filter); + +document + .getElementById("client_musiclist") + .addEventListener("change", musiclist_click); +document + .getElementById("role_select") + .addEventListener("change", () => changeRoleOOC()); +document + .getElementById("mute_select") + .addEventListener("change", mutelist_click); +document + .getElementById("bg_select") + .addEventListener("change", () => updateBackgroundPreview()); +document + .getElementById("evi_select") + .addEventListener("change", () => updateEvidenceIcon()); +document + .getElementById("client_mvolume") + .addEventListener("change", () => changeMusicVolume()); +document + .getElementById("client_bvolume") + .addEventListener("change", () => changeBlipVolume()); +document + .getElementById("client_themeselect") + .addEventListener("change", () => reloadTheme()); +const chatboxselect = document.getElementById( + "client_chatboxselect", +) as HTMLSelectElement; +chatboxselect.addEventListener("change", () => setChatbox(chatboxselect.value)); +document + .getElementById("client_iniselect") + .addEventListener("change", () => updateIniswap()); +document + .getElementById("client_callwords") + .addEventListener("change", () => changeCallwords()); + +document.addEventListener( + "error", + (e: Event) => { + const target = e.target; + if (!(target instanceof HTMLElement)) { + return; + } + + switch (target.dataset.error) { + case "img": + imgError(target as HTMLImageElement); + break; + case "char": + charError(target as HTMLImageElement); + break; + case "opus-check": + opusCheck(target as HTMLAudioElement); + break; + default: + break; + } + }, + true, +); + +document + .getElementById("client_inputbox") + .addEventListener("keypress", onEnter); +document + .getElementById("client_oocinputbox") + .addEventListener("keypress", onOOCEnter); + +document + .getElementById("client_sfxaudio") + .addEventListener("volumechange", changeSFXVolume); +document + .getElementById("client_shoutaudio") + .addEventListener("volumechange", changeShoutVolume); +document + .getElementById("client_testimonyaudio") + .addEventListener("volumechange", changeTestimonyVolume); diff --git a/webAO/packets/handlers/handleSI.ts b/webAO/packets/handlers/handleSI.ts index 369806b..c2a018e 100644 --- a/webAO/packets/handlers/handleSI.ts +++ b/webAO/packets/handlers/handleSI.ts @@ -22,9 +22,9 @@ export const handleSI = (args: string[]) => { demothing.className = "demothing"; demothing.loading = "lazy"; demothing.id = `demo_${i}`; - const demoonclick = document.createAttribute("onclick"); - demoonclick.value = `pickChar(${i})`; - demothing.setAttributeNode(demoonclick); + + demothing.dataset.action = "pick-char"; + demothing.dataset.id = String(i); document.getElementById("client_chartable")!.appendChild(demothing); } -- cgit