Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions public/news.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "2026-09-razer-chrome-153",
"level": "critical",
"message": "Using a Razer mouse? A recent Chrome update can stop it from being detected. This is a known issue we're tracking - open the workaround guide.",
"url": "https://openmouse.app/blog-razer-windows-chrome-153"
"id": "2026-09-openmouse-bridge",
"level": "info",
"message": "OpenMouse Bridge is here: Razer mice work on Windows again, plus game profiles, low-battery alerts and Firefox support. Read the announcement.",
"url": "https://openmouse.app/blog-openmouse-bridge"
}
53 changes: 0 additions & 53 deletions src/control.css
Original file line number Diff line number Diff line change
Expand Up @@ -4076,56 +4076,3 @@ body { height: 100vh; overflow: hidden; }
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

/* Self-XSS guard (src/self-xss-guard.ts). Shown only in production builds while
developer tools appear to be open; the copy steers the user away from pasting
unfamiliar code. */
#selfxss-notice {
position: fixed;
inset: 0;
z-index: 2147483000;
display: grid;
place-items: center;
padding: 1.5rem;
background: rgb(8 9 10 / 88%);
backdrop-filter: blur(4px);
}
.selfxss-panel {
max-width: 34rem;
padding: 1.75rem 1.9rem;
border: 1px solid var(--destructive);
border-radius: 14px;
background: var(--card);
box-shadow: 0 24px 60px rgb(0 0 0 / 55%);
text-align: center;
}
.selfxss-panel h2 {
margin: 0 0 0.75rem;
font-size: 2rem;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--destructive);
}
.selfxss-panel p {
margin: 0 0 1rem;
color: var(--dim);
line-height: 1.6;
}
.selfxss-hint {
font-weight: 600;
color: var(--text);
}
.selfxss-dismiss {
margin-top: 0.5rem;
padding: 0.6rem 1.1rem;
border: 1px solid var(--line-strong);
border-radius: 8px;
background: var(--raised);
color: var(--text-soft);
font-size: 0.78rem;
}
.selfxss-dismiss:hover {
background: var(--hover);
color: var(--text);
}
6 changes: 0 additions & 6 deletions src/security-headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
PERMISSIONS_POLICY,
SECURITY_HEADERS,
} from "../functions/_lib/security-headers.js";
import { devtoolsLikelyOpen } from "./self-xss-guard.ts";
import { TRUSTED_TYPES_POLICY, setSanitizedHtml } from "./trusted-types.ts";

const headersFile = readFileSync(new URL("../public/_headers", import.meta.url), "utf8");
Expand Down Expand Up @@ -121,8 +120,3 @@ test("the page ships no inline scripts for the CSP to block", () => {
assert.match(indexHtml, /<script[^>]*src="\/theme-init\.js"/);
});

test("the developer-tools heuristic only fires on a wide window/viewport gap", () => {
assert.equal(devtoolsLikelyOpen(1200, 1200, 800, 800), false);
assert.equal(devtoolsLikelyOpen(1600, 1200, 800, 800), true, "docked to the right");
assert.equal(devtoolsLikelyOpen(1200, 1200, 1000, 800), true, "docked to the bottom");
});
121 changes: 8 additions & 113 deletions src/self-xss-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
*
* A web page cannot stop someone from running code in their own developer
* tools — the browser exposes no such control — so this is deterrence, not a
* security boundary. It prints the familiar "do not paste code here" banner,
* and while developer tools appear to be open it shows a blocking notice.
* security boundary. It prints the familiar "do not paste code here" banner
* to the console.
*
* It deliberately avoids `debugger` loops and console overrides: those are
* trivially bypassed, break legitimate debugging, and can trap users. The
* developer-tools check is a heuristic (window vs. viewport size) and can be
* wrong (e.g. extreme browser zoom), so the notice can be dismissed.
* It deliberately avoids `debugger` loops, console overrides and detecting
* open developer tools: those are trivially bypassed, break legitimate
* debugging, and misfire on browsers with wide chrome (e.g. Firefox forks
* with vertical tabs or sidebars).
*
* Call only on production builds (see control.tsx).
*/
Expand All @@ -19,117 +19,12 @@ const WARNING_BODY =
"This console is meant for developers. If someone told you to copy and paste " +
"code here, it is a scam: pasting it can hand over your connected mouse and " +
"your OpenMouse settings. Never paste code you do not fully understand.";
const NOTICE_HINT = "Developer tools detected. Close them to continue.";
const DISMISS_LABEL = "I understand the risk — continue";

const DETECT_THRESHOLD_PX = 160;
const CHECK_INTERVAL_MS = 1000;
const DISMISS_KEY = "openmouse.selfxss.dismissed";

let notice: HTMLElement | null = null;
let dismissed = false;

/**
* Heuristic: when developer tools are docked, the outer window is much larger
* than the viewport. Pure so it can be unit tested.
*/
export function devtoolsLikelyOpen(
outerWidth: number,
innerWidth: number,
outerHeight: number,
innerHeight: number,
threshold: number = DETECT_THRESHOLD_PX,
): boolean {
return outerWidth - innerWidth > threshold || outerHeight - innerHeight > threshold;
}

function warn(): void {
/** Prints the production-only console warning. Call once. */
export function startSelfXssGuard(): void {
console.log(
`%c${WARNING_TITLE}`,
"color:#fff;background:#b3261e;font-size:24px;font-weight:800;padding:6px 14px;border-radius:6px;",
);
console.log(`%c${WARNING_BODY}`, "color:inherit;font-size:15px;line-height:1.5;");
}

function showNotice(): void {
if (notice) return;

const root = document.createElement("div");
root.id = "selfxss-notice";
root.setAttribute("role", "alertdialog");
root.setAttribute("aria-modal", "true");
root.setAttribute("aria-label", WARNING_TITLE);

const panel = document.createElement("div");
panel.className = "selfxss-panel";

const title = document.createElement("h2");
title.textContent = WARNING_TITLE;

const body = document.createElement("p");
body.textContent = WARNING_BODY;

const hint = document.createElement("p");
hint.className = "selfxss-hint";
hint.textContent = NOTICE_HINT;

const dismiss = document.createElement("button");
dismiss.type = "button";
dismiss.className = "selfxss-dismiss";
dismiss.textContent = DISMISS_LABEL;
dismiss.addEventListener("click", () => {
dismissed = true;
try {
sessionStorage.setItem(DISMISS_KEY, "1");
} catch {
/* storage unavailable — the dismissal simply does not persist */
}
hideNotice();
});

panel.append(title, body, hint, dismiss);
root.append(panel);
document.body.append(root);
notice = root;
}

function hideNotice(): void {
notice?.remove();
notice = null;
}

function storedDismissed(): boolean {
try {
return sessionStorage.getItem(DISMISS_KEY) === "1";
} catch {
return false;
}
}

/** Starts the production-only console/DevTools deterrence. Call once. */
export function startSelfXssGuard(): void {
if (typeof window === "undefined" || typeof document === "undefined") return;
dismissed = storedDismissed();
warn();

const check = (): void => {
if (dismissed) return;
if (
devtoolsLikelyOpen(
window.outerWidth,
window.innerWidth,
window.outerHeight,
window.innerHeight,
)
) {
showNotice();
} else {
hideNotice();
}
};

check();
window.setInterval(check, CHECK_INTERVAL_MS);
window.addEventListener("focus", check);
window.addEventListener("resize", check);
}
Loading