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.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user