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 | |
| 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.
| -rw-r--r-- | public/client.html | 126 | ||||
| -rw-r--r-- | webAO/client.ts | 1 | ||||
| -rw-r--r-- | webAO/dom-events.ts | 226 | ||||
| -rw-r--r-- | webAO/packets/handlers/handleSI.ts | 6 |
4 files changed, 276 insertions, 83 deletions
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 @@ </div> <div id="client_charselect"> <p>Choose your character</p> - <button onclick="pickChar(-1)">Or spectate</button> + <button data-action="pick-char" data-id="-1">Or spectate</button> <br /><br /> <input id="client_charactersearch" placeholder="Search" - oninput="chartable_filter(event)" /> <br /><br /> <div id="client_chartable"></div> @@ -124,7 +123,7 @@ <p id="client_error_code"> Error code: <span id="error_id">(none)</span> </p> - <button id="client_reconnect" onclick="ReconnectButton()"> + <button id="client_reconnect"> Reconnect </button> </div> @@ -139,57 +138,57 @@ <div id="client_fullview"> <img id="client_court" /> <div id="client_stitch_court"> - <img id="client_court_def" onerror="imgError(this);" /> - <img id="client_court_deft" onerror="imgError(this);" /> - <img id="client_court_wit" onerror="imgError(this);" /> - <img id="client_court_prot" onerror="imgError(this);" /> - <img id="client_court_pro" onerror="imgError(this);" /> + <img id="client_court_def" data-error="img" /> + <img id="client_court_deft" data-error="img" /> + <img id="client_court_wit" data-error="img" /> + <img id="client_court_prot" data-error="img" /> + <img id="client_court_pro" data-error="img" /> </div> <div id="client_def_pair_char" class="client_char" alt="Paired character" > - <img id="client_def_pair_img" onerror="charError(this);" /> + <img id="client_def_pair_img" data-error="char" /> </div> <div id="client_def_char" class="client_char" alt="Character"> - <img id="client_def_char_img" onerror="charError(this);" /> + <img id="client_def_char_img" data-error="char" /> </div> <div id="client_wit_pair_char" class="client_char" alt="Paired character" > - <img id="client_wit_pair_img" onerror="charError(this);" /> + <img id="client_wit_pair_img" data-error="char" /> </div> <div id="client_wit_char" class="client_char" alt="Character"> - <img id="client_wit_char_img" onerror="charError(this);" /> + <img id="client_wit_char_img" data-error="char" /> </div> <div id="client_pro_pair_char" class="client_char" alt="Paired character" > - <img id="client_pro_pair_img" onerror="charError(this);" /> + <img id="client_pro_pair_img" data-error="char" /> </div> <div id="client_pro_char" class="client_char" alt="Character"> - <img id="client_pro_char_img" onerror="charError(this);" /> + <img id="client_pro_char_img" data-error="char" /> </div> <img id="client_def_bench" class="client_bench" /> <img id="client_wit_bench" class="client_bench" /> <img id="client_pro_bench" class="client_bench" /> </div> <div id="client_classicview"> - <img id="client_court_classic" onerror="imgError(this);" /> + <img id="client_court_classic" data-error="img" /> <div id="client_pair_char" class="client_char" alt="Paired character" > - <img id="client_pair_img" onerror="charError(this);" /> + <img id="client_pair_img" data-error="char" /> </div> <div id="client_char" class="client_char" alt="Character"> - <img id="client_char_img" onerror="charError(this);" /> + <img id="client_char_img" data-error="char" /> </div> <img id="client_bench_classic" class="client_bench" /> </div> @@ -202,13 +201,13 @@ id="client_evi" src="" alt="Character Evidence" - onerror="imgError(this);" + data-error="img" /> <img id="client_testimony" alt="Testimony overlay" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" - onerror="imgError(this);" + data-error="img" /> <img id="client_shout" @@ -236,16 +235,13 @@ </div> </div> </div> - <form onsubmit="return false"> <input id="client_inputbox" class="long" type="text" - onkeypress="onEnter(event)" placeholder="Say something…" autocomplete="off" /> - </form> <div id="client_bars"> <span id="client_defense_hp" class="health-box"> <div class="health-bar"></div> @@ -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! </button> @@ -276,7 +273,8 @@ id="button_2" alt="Objection!" class="client_button" - onclick="toggleShout(2)" + data-action="toggle-shout" + data-id="2" > Objection! </button> @@ -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! </button> @@ -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 </button> @@ -328,8 +328,8 @@ id="button_flip" alt="Flip" class="client_button" - onclick="toggleEffect(this)" style="display: none" + data-action="toggle-effect" > Flip </button> @@ -337,7 +337,7 @@ id="button_flash" alt="Flash" class="client_button" - onclick="toggleEffect(this)" + data-action="toggle-effect" > Flash </button> @@ -345,8 +345,8 @@ id="button_shake" alt="Shake" class="client_button" - onclick="toggleEffect(this)" style="display: none" + data-action="toggle-effect" > Shake </button> @@ -358,7 +358,6 @@ <select id="role_select" name="role_select" - onchange="changeRoleOOC()" > <option value="">Default</option> <option value="def">Defense</option> @@ -376,7 +375,6 @@ id="char_change" alt="Change" class="client_button hover_button" - onclick="changeCharacter()" > Change Character </button> @@ -384,7 +382,6 @@ id="char_random" alt="Random" class="client_button hover_button" - onclick="randomCharacterOOC()" > Random Character </button> @@ -395,7 +392,6 @@ id="button_toggle_pairing" alt="Pairing" class="client_button" - onclick="toggleElement('pairing_settings')" > Pairing </button> @@ -442,7 +438,6 @@ id="pair_reset" type="button" value="Reset" - onclick="resetOffset()" /> </span> </span> @@ -457,7 +452,6 @@ id="showname" name="showname" type="checkbox" - onclick="showname_click()" checked /> <br /> @@ -496,7 +490,6 @@ <span id="judge_action" style="display: none"> <span id="menu_wt" - onclick="initWT()" class="judge_button" style="color: blue" ><i>Witness<br />Testimony</i></span @@ -504,7 +497,6 @@ <span id="menu_ce" - onclick="initCE()" class="judge_button" style="color: red" ><i>Cross<br />Examination</i></span @@ -512,7 +504,6 @@ <span id="menu_nguilty" - onclick="notguilty()" class="judge_button" style=" color: white; @@ -530,7 +521,6 @@ <span id="menu_guilty" - onclick="guilty()" class="judge_button" style="color: black; font-family: serif; font-size: 1.5em" >Guilty</span @@ -538,20 +528,20 @@ <br /> <span style="display: inline-block; vertical-align: middle"> - <span id="menu_rhpd" onclick="redHPD()" class="healthchange_button"> + <span id="menu_rhpd" class="healthchange_button"> ⊟ </span> <span style="font-size: 1.25em">Defense</span> - <span id="menu_ahpd" onclick="addHPD()" class="healthchange_button"> + <span id="menu_ahpd" class="healthchange_button"> ⊞ </span> </span> <span style="display: inline-block; vertical-align: middle"> - <span id="menu_ahpp" onclick="addHPP()" class="healthchange_button"> + <span id="menu_ahpp" class="healthchange_button"> ⊞ </span> <span style="font-size: 1.25em">Prosecution</span> - <span id="menu_rhpp" onclick="redHPP()" class="healthchange_button"> + <span id="menu_rhpp" class="healthchange_button"> ⊟ </span> </span> @@ -567,7 +557,6 @@ <select name="mute_select" id="mute_select" - onchange="mutelist_click(event)" ></select> </span> </fieldset> @@ -579,23 +568,23 @@ <div id="client_menu"> <div id="client_menu_buttons"> <div class="hrtext">Main Menu</div> - <span id="menu_1" onclick="toggleMenu(1)" class="menu_button active"> + <span id="menu_1" data-action="toggle-menu" data-id="1" class="menu_button active"> <b class="menu_icon">📍</b> <div class="menu_text">Areas</div> </span> - <span id="menu_2" onclick="toggleMenu(2)" class="menu_button"> + <span id="menu_2" data-action="toggle-menu" data-id="2" class="menu_button"> <b class="menu_icon">💼</b> <div class="menu_text">Evidence</div> </span> - <span id="menu_3" onclick="toggleMenu(3)" class="menu_button"> + <span id="menu_3" data-action="toggle-menu" data-id="3" class="menu_button"> <b class="menu_icon">🔧</b> <div class="menu_text">Settings</div> </span> - <span id="menu_4" onclick="toggleMenu(4)" class="menu_button"> + <span id="menu_4" data-action="toggle-menu" data-id="4" class="menu_button"> <b class="menu_icon">❓</b> <div class="menu_text">About</div> </span> - <span id="menu_cm" onclick="callMod()" class="menu_button"> + <span id="menu_cm" class="menu_button"> <b class="menu_icon">🚨</b> <div class="menu_text" style="color: #ce2727">Call Mod</div> </span> @@ -615,7 +604,7 @@ id="bg_preview" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" alt="Background Preview" - onerror="imgError(this);" + data-error="img" /> </span> <span style="display: inline-block"> @@ -624,7 +613,6 @@ <select id="bg_select" name="bg_select" - onchange="updateBackgroundPreview()" style="margin-top: 10px" ></select> <br /> @@ -640,7 +628,6 @@ id="bg_change" alt="Change" class="client_button hover_button" - onclick="changeBackgroundOOC()" > Change </button> @@ -686,7 +673,7 @@ class="evi_icon" src="" alt="Evidence Icon" - onerror="imgError(this);" + data-error="img" /> <div id="evi_options"> <input @@ -700,7 +687,6 @@ <select id="evi_select" name="evi_select" - onchange="updateEvidenceIcon()" ></select> <input id="evi_filename" @@ -725,7 +711,6 @@ id="evi_add" alt="Add Evidence" class="client_button hover_button" - onclick="addEvidence()" > Add </button> @@ -733,7 +718,6 @@ id="evi_edit" alt="Edit Evidence" class="client_button hover_button inactive" - onclick="editEvidence()" > Edit </button> @@ -741,7 +725,6 @@ id="evi_cancel" alt="Cancel Evidence" class="client_button hover_button inactive" - onclick="cancelEvidence()" > Cancel </button> @@ -749,7 +732,6 @@ id="evi_del" alt="Remove Evidence" class="client_button hover_button inactive" - onclick="deleteEvidence()" > Remove </button> @@ -762,7 +744,7 @@ id="button_present" alt="Present" class="client_button" - onclick="toggleEffect(this)" + data-action="toggle-effect" > Present </button> @@ -781,30 +763,26 @@ max="1" value="1" step="0.01" - onchange="changeMusicVolume()" /> <p>SFX</p> <audio id="client_sfxaudio" - onvolumechange="changeSFXVolume()" - onerror="opusCheck(this)" + data-error="opus-check" controls ></audio> <p>Shouts</p> <audio id="client_shoutaudio" - onvolumechange="changeShoutVolume()" - onerror="opusCheck(this)" + data-error="opus-check" controls ></audio> <p>Testimony/Guilty</p> <audio id="client_testimonyaudio" - onvolumechange="changeTestimonyVolume()" - onerror="opusCheck(this)" + data-error="opus-check" controls ></audio> @@ -817,7 +795,6 @@ max="1" value="1" step="0.01" - onchange="changeBlipVolume()" /> <br /> <br /> @@ -825,7 +802,6 @@ <select id="client_themeselect" name="client_themeselect" - onchange="reloadTheme()" > <option value="default" selected>Default</option> <option value="classic">Classic</option> @@ -840,7 +816,6 @@ <select id="client_chatboxselect" name="client_chatboxselect" - onchange="setChatbox(this.value)" > <option value="dynamic" selected>Use characters</option> <option value="aa">AA</option> @@ -883,10 +858,9 @@ <select id="client_iniselect" name="client_iniselect" - onchange="updateIniswap()" ></select> <input id="client_ininame" name="client_ininame" /> - <button id="client_inichange" onclick="iniedit()">Change</button> + <button id="client_inichange">Change</button> <br /> <br /> <p>Pan-tilt (experimental)</p> @@ -895,7 +869,6 @@ type="checkbox" id="client_pantilt" name="client_pantilt" - onclick="switchPanTilt()" /> <br /> <br /> @@ -905,7 +878,6 @@ type="checkbox" id="client_hdviewport" name="client_hdviewport" - onclick="switchAspectRatio()" /> <br /> <label for="client_hdviewport_offset">Offset chatbox:</label> @@ -913,7 +885,6 @@ type="checkbox" id="client_hdviewport_offset" id="client_hdviewport_offset" - onclick="switchChatOffset()" /> <br /> <br /> @@ -925,7 +896,6 @@ rows="4" cols="10" placeholder="Put 1 callword per line here" - onchange="changeCallwords()" ></textarea> <br /> <br /> @@ -941,7 +911,7 @@ </fieldset> <br /> <br /> - <button id="client_disconnect" onclick="DisconnectButton()"> + <button id="client_disconnect"> Disconnect </button> </span> @@ -993,7 +963,6 @@ <input id="client_oocinputbox" type="text" - onkeypress="onOOCEnter(event)" /> </span> <span @@ -1004,7 +973,6 @@ id="client_replaygo" style="width: 25%" type="button" - onclick="onReplayGo(event)" value="GO" /> <input @@ -1022,12 +990,10 @@ <input id="client_musicsearch" placeholder="Search" - oninput="musiclist_filter(event)" /> <select id="client_musiclist" size="5" - onchange="musiclist_click(event)" ></select> </template> 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); } |
