Sicherheits-Härtung (v1.1.1) und aktuelle README
- nur http(s)-Links, Validierung/Limits für Remote-, Storage- und Importdaten - HTTP-Sync nur im lokalen Netz, keine Zugangsdaten in der URL - Berechtigung bei Serverwechsel entziehen, explizite CSP, UUID-IDs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
bb7ee831a8
commit
a04aaae859
@@ -5,13 +5,71 @@ const LabSync = (() => {
|
||||
const FILE = "lablinks.json";
|
||||
const TOMB_TTL = 60 * 24 * 3600 * 1000;
|
||||
|
||||
const MAX_LINKS = 500;
|
||||
const MAX_TOMBS = 1000;
|
||||
const MAX_BODY = 1_000_000;
|
||||
const DAY = 24 * 3600 * 1000;
|
||||
|
||||
// Nur http(s) – verhindert javascript:, data:, file:, chrome: usw.
|
||||
function safeUrl(raw) {
|
||||
try {
|
||||
const u = new URL(String(raw));
|
||||
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
|
||||
return u.href.length <= 2048 ? u.href : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// Daten aus Storage, Remote-Datei oder Import sind nicht vertrauenswürdig.
|
||||
function cleanLink(x) {
|
||||
if (!x || typeof x !== "object") return null;
|
||||
const id = String(x.id ?? "");
|
||||
const url = safeUrl(x.url);
|
||||
const name = String(x.name ?? "").trim().slice(0, 80);
|
||||
if (!/^[\w-]{1,64}$/.test(id) || !url || !name) return null;
|
||||
const icon = typeof x.icon === "string" ? [...x.icon].slice(0, 2).join("") : "";
|
||||
const updated = Math.min(Math.max(Number(x.updated) || 0, 0), Date.now() + DAY);
|
||||
return { id, name, url, icon, updated };
|
||||
}
|
||||
function cleanTomb(x) {
|
||||
if (!x || typeof x !== "object" || !/^[\w-]{1,64}$/.test(String(x.id ?? ""))) return null;
|
||||
return { id: String(x.id), updated: Math.min(Math.max(Number(x.updated) || 0, 0), Date.now() + DAY) };
|
||||
}
|
||||
function cleanState(d) {
|
||||
const arr = (v) => (Array.isArray(v) ? v : []);
|
||||
return {
|
||||
links: arr(d.links).slice(0, MAX_LINKS).map(cleanLink).filter(Boolean),
|
||||
deleted: arr(d.deleted).slice(0, MAX_TOMBS).map(cleanTomb).filter(Boolean),
|
||||
modified: Math.min(Math.max(Number(d.modified) || 0, 0), Date.now() + DAY),
|
||||
};
|
||||
}
|
||||
|
||||
// Prüft die Server-URL. Basic-Auth über http:// nur im lokalen Netz erlauben.
|
||||
function isPrivateHost(h) {
|
||||
if (h === "localhost" || h === "[::1]" || !h.includes(".")) return true;
|
||||
if (/\.(local|lan|home\.arpa|internal|localdomain)$/.test(h)) return true;
|
||||
const m = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
||||
if (!m) return false;
|
||||
const [a, b] = [+m[1], +m[2]];
|
||||
return a === 10 || a === 127 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31) || (a === 169 && b === 254);
|
||||
}
|
||||
function checkServerUrl(raw) {
|
||||
let u;
|
||||
try { u = new URL(String(raw).trim()); } catch { throw new Error("Ungültige URL"); }
|
||||
if (u.protocol !== "https:" && u.protocol !== "http:") throw new Error("Nur http(s)-URLs erlaubt");
|
||||
if (u.username || u.password) throw new Error("Zugangsdaten bitte in die Felder, nicht in die URL");
|
||||
const insecure = u.protocol === "http:";
|
||||
if (insecure && !isPrivateHost(u.hostname))
|
||||
throw new Error("Unverschlüsseltes HTTP ist nur im lokalen Netz erlaubt – bitte HTTPS verwenden");
|
||||
return { origin: u.origin, insecure };
|
||||
}
|
||||
|
||||
const getConfig = async () => (await chrome.storage.local.get("webdav")).webdav || null;
|
||||
const setConfig = (cfg) =>
|
||||
cfg ? chrome.storage.local.set({ webdav: cfg }) : chrome.storage.local.remove("webdav");
|
||||
|
||||
async function readLocal() {
|
||||
const d = await chrome.storage.sync.get(["links", "deleted", "modified"]);
|
||||
return { links: d.links || [], deleted: d.deleted || [], modified: d.modified || 0 };
|
||||
return cleanState(d);
|
||||
}
|
||||
const writeLocal = (s) =>
|
||||
chrome.storage.sync.set({ links: s.links, deleted: s.deleted, modified: s.modified });
|
||||
@@ -46,13 +104,13 @@ const LabSync = (() => {
|
||||
const res = await dav(cfg, "GET", fileUrl(cfg));
|
||||
if (res.status === 404) return null;
|
||||
if (!res.ok) throw new Error(`Server antwortet mit HTTP ${res.status}`);
|
||||
const text = await res.text();
|
||||
if (text.length > MAX_BODY) throw new Error("Remote-Datei ist unerwartet groß");
|
||||
let data;
|
||||
try { data = await res.json(); } catch { throw new Error("Remote-Datei ist kein gültiges JSON"); }
|
||||
if (!data || !Array.isArray(data.links)) throw new Error("Remote-Datei hat ein unbekanntes Format");
|
||||
return {
|
||||
data: { links: data.links, deleted: data.deleted || [], modified: data.modified || 0 },
|
||||
etag: res.headers.get("ETag"),
|
||||
};
|
||||
try { data = JSON.parse(text); } catch { throw new Error("Remote-Datei ist kein gültiges JSON"); }
|
||||
if (!data || typeof data !== "object" || !Array.isArray(data.links))
|
||||
throw new Error("Remote-Datei hat ein unbekanntes Format");
|
||||
return { data: cleanState(data), etag: res.headers.get("ETag") };
|
||||
}
|
||||
|
||||
// -> "ok" | "conflict"
|
||||
@@ -108,6 +166,7 @@ const LabSync = (() => {
|
||||
async function doSync() {
|
||||
const cfg = await getConfig();
|
||||
if (!cfg || !cfg.url) return { skipped: true };
|
||||
checkServerUrl(cfg.url);
|
||||
|
||||
for (let attempt = 0; attempt < 4; attempt++) {
|
||||
const local = await readLocal();
|
||||
@@ -128,5 +187,5 @@ const LabSync = (() => {
|
||||
throw new Error("Sync-Konflikt, bitte erneut versuchen");
|
||||
}
|
||||
|
||||
return { getConfig, setConfig, readLocal, writeLocal, sync };
|
||||
return { getConfig, setConfig, readLocal, writeLocal, sync, safeUrl, cleanLink, checkServerUrl };
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user