diff options
| author | Osmium Sorcerer <os@sof.beauty> | 2026-06-06 02:07:05 +0000 |
|---|---|---|
| committer | Osmium Sorcerer <os@sof.beauty> | 2026-06-06 03:09:27 +0000 |
| commit | e0ce108e0806d18353ad85125b2b5f1b1c67e07d (patch) | |
| tree | 4e70de464db82bf28d42b10bf260ba7361402f55 /webAO | |
| parent | fd75f3116aa30eb4958cc747f944f202ec69a484 (diff) | |
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.
Diffstat (limited to 'webAO')
| -rw-r--r-- | webAO/client.ts | 1 | ||||
| -rw-r--r-- | webAO/dom-events.ts | 226 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleSI.ts | 6 |
3 files changed, 230 insertions, 3 deletions
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); } |
