(tag: string, source: string)
| 397 | }; |
| 398 | |
| 399 | const sanitizeAttributes = (tag: string, source: string): string => { |
| 400 | const kept: string[] = []; |
| 401 | for (const [name, rawValue] of parseAttributes(source)) { |
| 402 | // Event handlers are the one class worth naming explicitly. React's server |
| 403 | // renderer never emits them, so reaching this branch means the shape |
| 404 | // changed underneath us — exactly the case the allowlist exists to survive. |
| 405 | if (name.startsWith("on")) continue; |
| 406 | if (name === "id") { |
| 407 | if (rawValue !== "") kept.push(`id="${escapeAttribute(PREVIEW_ID_PREFIX + rawValue)}"`); |
| 408 | continue; |
| 409 | } |
| 410 | if (!ALLOWED_ATTRIBUTES.has(name)) continue; |
| 411 | // `value` is meaningful on a list item and is a form payload anywhere else. |
| 412 | if (name === "value" && tag !== "li") continue; |
| 413 | if (name === "style") { |
| 414 | const style = sanitizeStyle(decodeEntities(rawValue)); |
| 415 | if (style !== "") kept.push(`style="${escapeAttribute(style)}"`); |
| 416 | continue; |
| 417 | } |
| 418 | const value = rewriteLocalRef(name, rawValue); |
| 419 | if (value === "") { |
| 420 | if (LOCAL_REF_ATTRIBUTES.has(name)) continue; |
| 421 | kept.push(name); |
| 422 | continue; |
| 423 | } |
| 424 | // A URL-bearing value that survived the local-ref rewrite is still checked: |
| 425 | // no scheme of any kind belongs in a preview attribute. |
| 426 | if (/^\s*(?:javascript|data|vbscript|blob|file|https?):/i.test(value)) continue; |
| 427 | kept.push(`${name}="${escapeAttribute(value)}"`); |
| 428 | } |
| 429 | return kept.length === 0 ? "" : ` ${kept.join(" ")}`; |
| 430 | }; |
| 431 | |
| 432 | /** |
| 433 | * Whether a sanitized fragment is worth showing instead of the schematic. |
no test coverage detected