diff --git a/index.html b/index.html index a3d6868..b7cfc6b 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,11 @@ - - + + + Mode 7. A horror story. diff --git a/js/core/teletext.js b/js/core/teletext.js index 76c436b..6afda1a 100644 --- a/js/core/teletext.js +++ b/js/core/teletext.js @@ -80,6 +80,7 @@ export class TeletextTerminal { // Export the terminal class this._keyQueue = []; // Key event queue this._keyWaiters = []; // Promises waiting for a key + this._maxKeyQueue = 64; // Bound buffered input so key repeat cannot grow memory indefinitely this._audioCtx = null; // AudioContext (lazy) this._muted = false; // Mute flag @@ -806,6 +807,9 @@ this.setInputMode(false); // Exit input mode after input is captured _pushKey(evt) { // Push a key event into the queue or resolve a waiter +if (!evt || !["enter", "backspace", "char"].includes(evt.type)) return; // Reject malformed events +if (evt.type === "char" && (typeof evt.ch !== "string" || evt.ch.length !== 1)) return; // Require one character + if (evt && evt.type === "enter") { // Enter triggers fast-forward this._fastForward = true; // Enable fast-forward this._fastForwardUntil = Date.now() + this._fastForwardMs; // Set expiry @@ -821,6 +825,7 @@ if (evt && evt.type === "enter") { // Enter triggers fast-forward return; // Done } // End waiter path + if (this._keyQueue.length >= this._maxKeyQueue) this._keyQueue.shift(); // Discard oldest buffered input this._keyQueue.push(evt); // Otherwise queue it for later } // End _pushKey diff --git a/tests/security.test.mjs b/tests/security.test.mjs new file mode 100644 index 0000000..1b085ff --- /dev/null +++ b/tests/security.test.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { TeletextTerminal } from "../js/core/teletext.js"; + +function makeInputTarget() { + const term = Object.create(TeletextTerminal.prototype); + term._keyQueue = []; + term._keyWaiters = []; + term._maxKeyQueue = 64; + term._fastForward = false; + term._fastForwardAnyKey = false; + term._fastForwardMs = 1200; + term._fastForwardUntil = 0; + return term; +} + +test("keyboard input queue remains bounded", () => { + const term = makeInputTarget(); + + for (let i = 0; i < 1000; i += 1) { + term._pushKey({ type: "char", ch: String(i % 10) }); + } + + assert.equal(term._keyQueue.length, 64); +}); + +test("malformed synthetic key events are rejected", () => { + const term = makeInputTarget(); + + term._pushKey(null); + term._pushKey({ type: "unknown" }); + term._pushKey({ type: "char", ch: "too long" }); + + assert.deepEqual(term._keyQueue, []); +}); + +test("entry point defines restrictive browser policies", async () => { + const html = await readFile(new URL("../index.html", import.meta.url), "utf8"); + + assert.match(html, /name="referrer" content="no-referrer"/); + assert.match(html, /http-equiv="Content-Security-Policy"/); + assert.match(html, /script-src 'self'/); + assert.match(html, /object-src 'none'/); + assert.match(html, /base-uri 'none'/); +});