aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOsmium Sorcerer <os@sof.beauty>2026-06-03 11:23:33 +0000
committerOsmium Sorcerer <os@sof.beauty>2026-06-06 03:09:27 +0000
commitfd75f3116aa30eb4958cc747f944f202ec69a484 (patch)
tree2afb99a17a2fe3c832c8eae0f0e7594ea806b7e9
parentbd8b53cd6046cef9802d593d8257392d81afb5ce (diff)
Remove safeTags, decodeChat, and prepChat
Following the removal of innerHTML manipulation, we no longer need these sanitization functions. I've reviewed every safeTags call site to make sure the outputs don't end up anywhere unsafe, and malicious input can't malipulate DOM or execute code. These values either end up either as plain text (textContent, innerText, createTextNode, title, option) or as a URL path to request assets to the server (encoded using encodeURI). That is, if safeTags was even effective, considering all that function did was replace '<' and '>' symbols with Unicode lookalikes. Even the comment was suggesting the use of fundamentally safer functions instead of these hacks. Replace remaining uses of prepChat with unescapeChat as we still need to do the token substitution (like "<and>" to "&"). decodeChat was escaping Unicode sequences like \uXXXX, but I don't see the reason for this, AO2 Client doesn't have this feature, and considering WebSocket text frames are strictly UTF-8, we don't need these encodings.
-rw-r--r--webAO/__tests__/encoding.test.ts39
-rw-r--r--webAO/client/addTrack.ts4
-rw-r--r--webAO/client/createArea.ts4
-rw-r--r--webAO/client/handleBans.ts2
-rw-r--r--webAO/client/handleCharacterInfo.ts25
-rw-r--r--webAO/encoding.ts33
-rw-r--r--webAO/master.ts2
-rw-r--r--webAO/packets/handlers/handleARUP.ts11
-rw-r--r--webAO/packets/handlers/handleBN.ts5
-rw-r--r--webAO/packets/handlers/handleCT.ts2
-rw-r--r--webAO/packets/handlers/handleLE.ts6
-rw-r--r--webAO/packets/handlers/handleMC.ts4
-rw-r--r--webAO/packets/handlers/handleMS.ts30
-rw-r--r--webAO/packets/handlers/handleZZ.ts2
14 files changed, 45 insertions, 124 deletions
diff --git a/webAO/__tests__/encoding.test.ts b/webAO/__tests__/encoding.test.ts
index 36f34cf..aa1409a 100644
--- a/webAO/__tests__/encoding.test.ts
+++ b/webAO/__tests__/encoding.test.ts
@@ -1,4 +1,4 @@
-import { escapeChat, unescapeChat, safeTags, decodeChat, prepChat } from '../encoding';
+import { escapeChat, unescapeChat } from '../encoding';
describe('encode/decode', () => {
it('should escape special characters correctly', () => {
@@ -13,40 +13,3 @@ describe('encode/decode', () => {
expect(unescapeChat(input)).toBe(expectedOutput);
});
});
-
-describe('safeTags', () => {
- it('should replace < with < and > with >', () => {
- const input = '<div>Hello</div>';
- const expectedOutput = '<div>Hello</div>';
- expect(safeTags(input)).toBe(expectedOutput);
- });
-
- it('should handle empty strings correctly', () => {
- expect(safeTags('')).toBe('');
- });
-});
-
-describe('decodeChat', () => {
- it('should decode escaped unicode characters', () => {
- const input = '\\u0041\\u0026\\u003F';
- const expectedOutput = 'A&?';
- expect(decodeChat(input)).toBe(expectedOutput);
- });
-
- it('should handle no unicode to decode', () => {
- const input = 'Hello World!';
- expect(decodeChat(input)).toBe(input);
- });
-});
-
-describe('prepChat', () => {
- it('should apply safeTags, unescapeChat and decodeChat correctly', () => {
- const input = '<num><and>A<percent>';
- const expectedOutput = '#&A%'; // Output after applying all functions in order
- expect(prepChat(input)).toBe(expectedOutput);
- });
-
- it('should handle empty strings correctly', () => {
- expect(prepChat('')).toBe('');
- });
-}); \ No newline at end of file
diff --git a/webAO/client/addTrack.ts b/webAO/client/addTrack.ts
index 09ca049..bba4b08 100644
--- a/webAO/client/addTrack.ts
+++ b/webAO/client/addTrack.ts
@@ -1,11 +1,11 @@
import { client } from "../client";
-import { unescapeChat, safeTags } from "../encoding";
+import { unescapeChat } from "../encoding";
import { getFilenameFromPath } from "../utils/paths";
export const addTrack = (trackname: string) => {
const newentry = <HTMLOptionElement>document.createElement("OPTION");
const songName = getFilenameFromPath(trackname);
- newentry.text = safeTags(unescapeChat(songName));
+ newentry.text = unescapeChat(songName);
newentry.value = trackname;
(<HTMLSelectElement>document.getElementById("client_musiclist")).options.add(
newentry,
diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts
index a90b49a..6bc00c0 100644
--- a/webAO/client/createArea.ts
+++ b/webAO/client/createArea.ts
@@ -1,9 +1,7 @@
import { client } from "../client";
import { area_click } from "../dom/areaClick";
-import { safeTags } from "../encoding";
-export const createArea = (id: number, aname: string) => {
- const name = safeTags(aname);
+export const createArea = (id: number, name: string) => {
const thisarea = {
name,
players: 0,
diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts
index 4abd1f0..f9a005b 100644
--- a/webAO/client/handleBans.ts
+++ b/webAO/client/handleBans.ts
@@ -1,5 +1,3 @@
-import { safeTags } from "../encoding";
-
/**
* Handles the kicked packet
* @param {string} type is it a kick or a ban
diff --git a/webAO/client/handleCharacterInfo.ts b/webAO/client/handleCharacterInfo.ts
index c7bcc94..278c6a7 100644
--- a/webAO/client/handleCharacterInfo.ts
+++ b/webAO/client/handleCharacterInfo.ts
@@ -1,5 +1,4 @@
import { client } from "../client";
-import { safeTags } from "../encoding";
import iniParse from "../iniParse";
import request from "../services/request";
import { AO_HOST } from "./aoHost";
@@ -22,17 +21,17 @@ export const setupCharacterBasic = (chargs: string[], charid: number) => {
const mute_select = <HTMLSelectElement>(
document.getElementById("mute_select")
);
- mute_select.add(new Option(safeTags(chargs[0]), String(charid)));
+ mute_select.add(new Option(chargs[0], String(charid)));
const pair_select = <HTMLSelectElement>(
document.getElementById("pair_select")
);
- pair_select.add(new Option(safeTags(chargs[0]), String(charid)));
+ pair_select.add(new Option(chargs[0], String(charid)));
// Store defaults — these get replaced with actual ini values by ensureCharIni
client.chars[charid] = {
- name: safeTags(chargs[0]),
- showname: safeTags(chargs[0]),
- desc: safeTags(chargs[1]),
+ name: chargs[0],
+ showname: chargs[0],
+ desc: chargs[1],
blips: "m",
gender: "",
side: "def",
@@ -86,14 +85,14 @@ export const ensureCharIni = async (charid: number): Promise<any> => {
cini.emotions = Object.assign(default_emotions, cini.emotions);
// Replace defaults with actual ini values
- char.showname = safeTags(cini.options.showname);
- char.blips = safeTags(cini.options.blips);
- char.gender = safeTags(cini.options.gender);
- char.side = safeTags(cini.options.side);
+ char.showname = cini.options.showname;
+ char.blips = cini.options.blips;
+ char.gender = cini.options.gender;
+ char.side = cini.options.side;
char.chat =
cini.options.chat === ""
- ? safeTags(cini.options.category)
- : safeTags(cini.options.chat);
+ ? cini.options.category
+ : cini.options.chat;
char.icon = img ? img.src : "";
char.inifile = cini;
@@ -124,7 +123,7 @@ export const handleCharacterInfo = async (chargs: string[], charid: number) => {
// Reset inifile so ensureCharIni will re-fetch
if (client.chars[charid]) {
- client.chars[charid].name = safeTags(chargs[0]);
+ client.chars[charid].name = chargs[0];
client.chars[charid].inifile = null;
} else {
setupCharacterBasic(chargs, charid);
diff --git a/webAO/encoding.ts b/webAO/encoding.ts
index 37f064a..1c380f7 100644
--- a/webAO/encoding.ts
+++ b/webAO/encoding.ts
@@ -21,36 +21,3 @@ export function unescapeChat(estring: string): string {
.replaceAll("<percent>", "%")
.replaceAll("<dollar>", "$");
}
-
-/**
- * Escapes a string to be HTML-safe.
- *
- * XXX: This is unnecessary if we use `createTextNode` instead!
- * @param {string} unsafe an unsanitized string
- */
-export function safeTags(unsafe: string): string {
- if (unsafe) {
- return unsafe.replaceAll(">", ">").replaceAll("<", "<");
- }
- return "";
-}
-
-/**
- * Decodes text on client side.
- * @param {string} estring the string to be decoded
- */
-export function decodeChat(estring: string): string {
- // Source: https://stackoverflow.com/questions/7885096/how-do-i-decode-a-string-with-escaped-unicode
- return estring.replace(/\\u([\d\w]{1,})/gi, (match, group) =>
- String.fromCharCode(parseInt(group, 16)),
- );
-}
-
-/**
- * XXX: a nasty hack made by gameboyprinter.
- * @param {string} msg chat message to prepare for display
- */
-export function prepChat(msg: string): string {
- // TODO: make this less awful
- return safeTags(unescapeChat(decodeChat(msg)));
-}
diff --git a/webAO/master.ts b/webAO/master.ts
index 569795e..fd7b9a6 100644
--- a/webAO/master.ts
+++ b/webAO/master.ts
@@ -1,5 +1,3 @@
-import { safeTags } from "./encoding";
-
interface AOServer {
name: string;
description: string;
diff --git a/webAO/packets/handlers/handleARUP.ts b/webAO/packets/handlers/handleARUP.ts
index 7f1c701..64dc586 100644
--- a/webAO/packets/handlers/handleARUP.ts
+++ b/webAO/packets/handlers/handleARUP.ts
@@ -1,5 +1,4 @@
import { client } from "../../client";
-import { safeTags } from "../../encoding";
/**
* Handle the change of players in an area.
@@ -12,17 +11,17 @@ export const handleARUP = (args: string[]) => {
// the server sends us ARUP before we even get the area list
const thisarea = document.getElementById(`area${i}`)!;
switch (Number(args[0])) {
- case 0: // playercount
+ case 0:
client.areas[i].players = Number(args[i + 1]);
break;
- case 1: // status
- client.areas[i].status = safeTags(args[i + 1]);
+ case 1:
+ client.areas[i].status = args[i + 1];
break;
case 2:
- client.areas[i].cm = safeTags(args[i + 1]);
+ client.areas[i].cm = args[i + 1];
break;
case 3:
- client.areas[i].locked = safeTags(args[i + 1]);
+ client.areas[i].locked = args[i + 1];
break;
}
diff --git a/webAO/packets/handlers/handleBN.ts b/webAO/packets/handlers/handleBN.ts
index b9498b7..20b25cd 100644
--- a/webAO/packets/handlers/handleBN.ts
+++ b/webAO/packets/handlers/handleBN.ts
@@ -1,6 +1,5 @@
import { client } from "../../client";
import { AO_HOST } from "../../client/aoHost";
-import { safeTags } from "../../encoding";
import { updateBackgroundPreview } from "../../dom/updateBackgroundPreview";
import { getIndexFromSelect } from "../../dom/getIndexFromSelect";
import { switchPanTilt } from "../../dom/switchPanTilt";
@@ -13,7 +12,7 @@ import { setBackgroundImage } from "../../viewport/utils/setSide"
*/
export const handleBN = (args: string[]) => {
- const bgFromArgs = safeTags(args[1]);
+ const bgFromArgs = args[1];
client.viewport.setBackgroundName(bgFromArgs);
const bgfolder = client.viewport.getBackgroundFolder();
const bg_index = getIndexFromSelect(
@@ -43,7 +42,7 @@ export const handleBN = (args: string[]) => {
setBackgroundImage("client_court_prot",args[1],"transition_pro")
setBackgroundImage("client_court",args[1],"court")
-
+
if((<HTMLImageElement>document.getElementById("client_court")).src !== transparentPng) {
(<HTMLInputElement>document.getElementById("client_pantilt")).checked =
true;
diff --git a/webAO/packets/handlers/handleCT.ts b/webAO/packets/handlers/handleCT.ts
index d4868bd..4f2857e 100644
--- a/webAO/packets/handlers/handleCT.ts
+++ b/webAO/packets/handlers/handleCT.ts
@@ -1,5 +1,5 @@
import queryParser from "../../utils/queryParser";
-import { prepChat } from "../../encoding";
+import { unescapeChat } from "../../encoding";
const { mode } = queryParser();
/**
diff --git a/webAO/packets/handlers/handleLE.ts b/webAO/packets/handlers/handleLE.ts
index dd2a2f1..e79bcd5 100644
--- a/webAO/packets/handlers/handleLE.ts
+++ b/webAO/packets/handlers/handleLE.ts
@@ -1,6 +1,6 @@
import { client } from "../../client";
import { AO_HOST } from "../../client/aoHost";
-import { prepChat, safeTags } from "../../encoding";
+import { unescapeChat } from "../../encoding";
/**
* Handles incoming evidence list, all evidences at once
@@ -14,8 +14,8 @@ export const handleLE = (args: string[]) => {
if (!args[i].includes("&")) break;
const arg = args[i].split("&");
client.evidences[i - 1] = {
- name: prepChat(arg[0]),
- desc: prepChat(arg[1]),
+ name: unescapeChat(arg[0]),
+ desc: unescapeChat(arg[1]),
filename: arg[2],
icon: `${AO_HOST}evidence/${encodeURI(arg[2])}`,
};
diff --git a/webAO/packets/handlers/handleMC.ts b/webAO/packets/handlers/handleMC.ts
index aaa30a1..f9e1cca 100644
--- a/webAO/packets/handlers/handleMC.ts
+++ b/webAO/packets/handlers/handleMC.ts
@@ -1,4 +1,4 @@
-import { prepChat } from "../../encoding";
+import { unescapeChat } from "../../encoding";
import { client } from "../../client";
import { AO_HOST } from "../../client/aoHost";
import { appendICLog } from "../../client/appendICLog";
@@ -8,7 +8,7 @@ import { appendICLog } from "../../client/appendICLog";
* @param {Array} args packet arguments
*/
export const handleMC = (args: string[]) => {
- const track = prepChat(args[1]);
+ const track = unescapeChat(args[1]);
let charID = Number(args[2]);
const showname = args[3] || "";
const looping = Boolean(Number(args[4])) || false;
diff --git a/webAO/packets/handlers/handleMS.ts b/webAO/packets/handlers/handleMS.ts
index 5a9d789..ab2e1d1 100644
--- a/webAO/packets/handlers/handleMS.ts
+++ b/webAO/packets/handlers/handleMS.ts
@@ -3,7 +3,7 @@
import { client, extrafeatures, UPDATE_INTERVAL } from "../../client";
import { handleCharacterInfo, ensureCharIni } from "../../client/handleCharacterInfo";
import { resetICParams } from "../../client/resetICParams";
-import { prepChat, safeTags } from "../../encoding";
+import { unescapeChat } from "../../encoding";
import { handle_ic_speaking } from "../../viewport/utils/handleICSpeaking";
/**
* Handles an in-character chat message.
@@ -13,7 +13,7 @@ export const handleMS = (args: string[]) => {
// duplicate message
if (args[5] !== client.viewport.getChatmsg().content) {
const char_id = Number(args[9]);
- const char_name = safeTags(args[3]);
+ const char_name = args[3];
let msg_nameplate = args[3];
let msg_blips = "m";
@@ -58,21 +58,21 @@ export const handleMS = (args: string[]) => {
if (char_muted === false) {
let chatmsg = {
- deskmod: Number(safeTags(args[1])),
- preanim: safeTags(args[2]), // get preanim
+ deskmod: Number(args[1]),
+ preanim: args[2],
nameplate: msg_nameplate,
chatbox: char_chatbox,
name: char_name,
- sprite: safeTags(args[4]),
- content: prepChat(args[5]), // Escape HTML tags
+ sprite: args[4],
+ content: unescapeChat(args[5]),
side: args[6],
- sound: safeTags(args[7]),
- blips: safeTags(msg_blips),
+ sound: args[7],
+ blips: msg_blips,
type: Number(args[8]),
charid: char_id,
snddelay: Number(args[10]),
objection: Number(args[11]),
- evidence: Number(safeTags(args[12])),
+ evidence: Number(args[12]),
flip: Number(args[13]),
flash: Number(args[14]),
color: Number(args[15]),
@@ -81,10 +81,10 @@ export const handleMS = (args: string[]) => {
if (args.length > 16) {
const extra_cccc = {
- showname: prepChat(args[16]),
+ showname: unescapeChat(args[16]),
other_charid: Number(args[17]),
- other_name: safeTags(args[18]),
- other_emote: safeTags(args[19]),
+ other_name: args[18],
+ other_emote: args[19],
self_offset: args[20].split("&"),
other_offset: args[21].split("&"),
other_flip: Number(args[22]),
@@ -96,9 +96,9 @@ export const handleMS = (args: string[]) => {
const extra_27 = {
looping_sfx: Number(args[24]),
screenshake: Number(args[25]),
- frame_screenshake: safeTags(args[26]),
- frame_realization: safeTags(args[27]),
- frame_sfx: safeTags(args[28]),
+ frame_screenshake: args[26],
+ frame_realization: args[27],
+ frame_sfx: args[28],
};
chatmsg = Object.assign(extra_27, chatmsg);
diff --git a/webAO/packets/handlers/handleZZ.ts b/webAO/packets/handlers/handleZZ.ts
index 66e7d15..a1a0768 100644
--- a/webAO/packets/handlers/handleZZ.ts
+++ b/webAO/packets/handlers/handleZZ.ts
@@ -1,6 +1,6 @@
import { client } from "../../client";
import { AO_HOST } from "../../client/aoHost";
-import { prepChat } from "../../encoding";
+import { unescapeChat } from "../../encoding";
/**
* Handles a modcall