Compare commits

..

2 Commits

Author SHA1 Message Date
egutierrez 472fa25bae build(frontend): rebuild dist with XSS scheme guard 2026-05-27 11:04:20 +02:00
egutierrez aab4f12fc4 fix(0128): XSS scheme allowlist + drop dead fileID
review findings:
- MessageBody: only http(s) and relative paths allowed for links;
  data:image/* allowed for inline images. Rejects javascript:,
  data:text/html, vbscript: which would execute via <a href>.
  Unsafe matches fall back to plain text.
- files.go: remove unused fileID var generated then discarded.
2026-05-27 11:04:20 +02:00
4 changed files with 166 additions and 143 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kanban</title>
<script type="module" crossorigin src="/assets/index-ZZWeneKP.js"></script>
<script type="module" crossorigin src="/assets/index-DT3pghXY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-b0xjFtx2.css">
</head>
<body>
-5
View File
@@ -197,7 +197,6 @@ func handleUploadCardFile(db *DB, workdir string) http.HandlerFunc {
}
fname := safeFilename(header.Filename)
fileID := newID()
storedPath := filepath.Join(dir, randomFilePrefix()+"__"+fname)
out, err := os.Create(storedPath)
@@ -237,16 +236,12 @@ func handleUploadCardFile(db *DB, workdir string) http.HandlerFunc {
actor, _ := infra.UserIDFromContext(r.Context(), userCtxKey)
// Use the random-prefixed path on disk but a stable file id in the DB.
cf, err := db.CreateCardFile(cardID, actor, fname, mimeType, storedPath, source, written)
if err != nil {
os.Remove(storedPath)
serverError(w, err)
return
}
// We generated newID() in CreateCardFile; align the on-disk filename with that id
// is not required since stored_path is what we serve from.
_ = fileID
infra.HTTPJSONResponse(w, http.StatusCreated, cf)
}
+30 -2
View File
@@ -14,6 +14,24 @@ type Token = ImgToken | LinkToken | TextToken;
const TOKEN_RE = /(!\[([^\]\n]*)\]\(([^)\s]+)\))|(\[([^\]\n]+)\]\(([^)\s]+)\))/g;
// Allow only safe URL schemes. Reject javascript:, data:text/html, vbscript:, etc.
// Accepts: absolute http(s), protocol-relative //, and same-origin paths (/...).
function safeURL(url: string): string | null {
const u = url.trim();
if (u.startsWith("/")) return u;
if (/^https?:\/\//i.test(u)) return u;
return null;
}
// data: scheme is allowed only when the MIME prefix is image/.
function safeImageURL(url: string): string | null {
const safe = safeURL(url);
if (safe) return safe;
const u = url.trim();
if (/^data:image\/[a-z0-9.+-]+(;[a-z0-9-]+=[^,]+)*;base64,/i.test(u)) return u;
return null;
}
function tokenize(input: string): Token[] {
const out: Token[] = [];
let last = 0;
@@ -24,9 +42,19 @@ function tokenize(input: string): Token[] {
out.push({ kind: "text", value: input.slice(last, m.index) });
}
if (m[1]) {
out.push({ kind: "img", alt: m[2] || "", url: m[3] });
const url = safeImageURL(m[3]);
if (url) {
out.push({ kind: "img", alt: m[2] || "", url });
} else {
out.push({ kind: "text", value: m[0] });
}
} else if (m[4]) {
out.push({ kind: "link", label: m[5], url: m[6] });
const url = safeURL(m[6]);
if (url) {
out.push({ kind: "link", label: m[5], url });
} else {
out.push({ kind: "text", value: m[0] });
}
}
last = TOKEN_RE.lastIndex;
}