Harden CSRF/CSP and add hash version upgrades

This commit is contained in:
2026-02-18 20:51:18 +01:00
parent 3c7f3d2114
commit a130cba41a
23 changed files with 627 additions and 57 deletions

View File

@@ -47,6 +47,16 @@
display: block;
padding: 0;
}
.card-visual.has-image {
background: #f6b24f;
overflow: hidden;
}
.card-visual-image {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.card-visual.hovering {
cursor: zoom-in;
}
@@ -269,3 +279,10 @@ input[type="range"].full-slider:disabled::-moz-range-thumb {
background: #f1f1f1;
border-color: #c1c1c1;
}
.fx-canvas {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 120;
}

View File

@@ -205,6 +205,11 @@ Registriere dich erneut mit dem korrekten Schlüssel vom Host oder lasse das
Auth- und Admin-sensitive Routen sind gegen Brute-Force-Angriffe rate-limitiert.
Warte kurz und versuche es dann erneut.
### „CSRF-Validierung fehlgeschlagen."
Authentifizierte Schreibaktionen erfordern jetzt eine Same-Origin-Browseranfrage.
Lade die Seite neu und versuche es erneut. Bei eigener API-Nutzung müssen `Origin`/`Referer` zum App-Host passen.
## Daten & Datenschutz
- Vorschläge, Stimmen und Phasenstatus werden in einer gemeinsamen Datenbank gespeichert.

View File

@@ -209,6 +209,11 @@ Register again using the correct key from the host or leave it blank to crea
Auth and admin-sensitive routes are rate-limited to reduce brute-force attempts.
Wait briefly, then retry.
### "CSRF validation failed."
Authenticated write actions now require a same-origin browser request.
Reload the page and retry. If you're calling the API from custom tooling, send matching `Origin`/`Referer` values for your app host.
## Data & Privacy
- Suggestions, votes, and phase states are stored in a shared database.

47
wwwroot/js/effects.js vendored
View File

@@ -3,48 +3,15 @@
// Screenshot hover ---------------------------------------------------
export function setupCardVisualHover(el, url) {
if (!el || !url) return;
const img = new Image();
let naturalW = 0;
let naturalH = 0;
let loaded = false;
img.src = url;
img.onload = () => {
naturalW = img.naturalWidth;
naturalH = img.naturalHeight;
loaded = true;
};
const reset = () => {
el.classList.remove("hovering");
el.style.backgroundSize = "";
el.style.backgroundPosition = "";
el.style.backgroundRepeat = "";
};
el.addEventListener("mouseenter", () => {
el.classList.add("hovering");
el.style.backgroundSize = "auto";
el.style.backgroundRepeat = "no-repeat";
el.style.backgroundPosition = "center";
});
el.addEventListener("mousemove", (e) => {
if (!loaded) return;
const rect = el.getBoundingClientRect();
const overW = naturalW - rect.width;
const overH = naturalH - rect.height;
if (overW <= 0 && overH <= 0) {
el.style.backgroundPosition = "center";
return;
}
const xRatio = (e.clientX - rect.left) / rect.width;
const yRatio = (e.clientY - rect.top) / rect.height;
const xPercent = overW > 0 ? xRatio * 100 : 50;
const yPercent = overH > 0 ? yRatio * 100 : 50;
el.style.backgroundPosition = `${xPercent}% ${yPercent}%`;
});
["mouseleave", "blur"].forEach((evt) => el.addEventListener(evt, reset));
["mouseleave", "blur"].forEach((evt) =>
el.addEventListener(evt, () => {
el.classList.remove("hovering");
}),
);
}
// Celebration FX -----------------------------------------------------
@@ -57,10 +24,6 @@ function ensureFxCanvas() {
if (fxCanvas) return;
fxCanvas = document.createElement("canvas");
fxCanvas.className = "fx-canvas";
fxCanvas.style.position = "fixed";
fxCanvas.style.inset = "0";
fxCanvas.style.pointerEvents = "none";
fxCanvas.style.zIndex = "120";
fxCanvas.width = window.innerWidth;
fxCanvas.height = window.innerHeight;
fxCtx = fxCanvas.getContext("2d");

View File

@@ -6,7 +6,6 @@ import { setupCardVisualHover, triggerCelebration } from "./effects.js";
import { renderAdminLinker } from "./admin-ui.js";
import { getUiRuntime } from "./ui-runtime.js";
import {
cssEscapeUrl,
escapeHtml,
isLinked,
linkedPeerTitles,
@@ -95,7 +94,7 @@ export function buildCard(
: "";
const visual =
hasImage && safeShot
? `<button class="card-visual" data-img="${safeShot}" aria-label="${t("card.openScreenshot")}" style="background-image:url('${cssEscapeUrl(safeShot)}')"></button>`
? `<button class="card-visual has-image" data-img="${escapeHtml(safeShot)}" aria-label="${t("card.openScreenshot")}"><img class="card-visual-image" src="${escapeHtml(safeShot)}" alt="" loading="lazy" decoding="async" /></button>`
: `<div class="card-visual"></div>`;
const hasPlayers = s.minPlayers || s.maxPlayers;
const players = hasPlayers