aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
authorOsmium Sorcerer <os@sof.beauty>2026-06-06 02:07:05 +0000
committerOsmium Sorcerer <os@sof.beauty>2026-06-06 03:09:27 +0000
commite0ce108e0806d18353ad85125b2b5f1b1c67e07d (patch)
tree4e70de464db82bf28d42b10bf260ba7361402f55 /webAO
parentfd75f3116aa30eb4958cc747f944f202ec69a484 (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.ts1
-rw-r--r--webAO/dom-events.ts226
-rw-r--r--webAO/packets/handlers/handleSI.ts6
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);
}