256 lines
7.7 KiB
JavaScript
256 lines
7.7 KiB
JavaScript
|
|
import { u as unpackMeta } from './unhead.h_KkEIE6.mjs';
|
||
|
|
import { h as hasContent } from './unhead.AvDFlk_u.mjs';
|
||
|
|
import { i as isUnsafeKey } from './unhead.ChXiQvI8.mjs';
|
||
|
|
|
||
|
|
function defineHeadPlugin(plugin) {
|
||
|
|
return plugin;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FlatMetaPlugin = /* @__PURE__ */ defineHeadPlugin({
|
||
|
|
key: "flatMeta",
|
||
|
|
hooks: {
|
||
|
|
"entries:normalize": (ctx) => {
|
||
|
|
const tagsToAdd = [];
|
||
|
|
ctx.tags = ctx.tags.map((t) => {
|
||
|
|
if (t.tag !== "_flatMeta") {
|
||
|
|
return t;
|
||
|
|
}
|
||
|
|
tagsToAdd.push(unpackMeta(t.props).map((p) => ({
|
||
|
|
...t,
|
||
|
|
tag: "meta",
|
||
|
|
props: p
|
||
|
|
})));
|
||
|
|
return false;
|
||
|
|
}).filter(Boolean).concat(...tagsToAdd);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
const WhitelistAttributes = {
|
||
|
|
htmlAttrs: /* @__PURE__ */ new Set(["class", "style", "lang", "dir"]),
|
||
|
|
bodyAttrs: /* @__PURE__ */ new Set(["class", "style"]),
|
||
|
|
meta: /* @__PURE__ */ new Set(["name", "property", "charset", "content", "media"]),
|
||
|
|
noscript: /* @__PURE__ */ new Set([]),
|
||
|
|
style: /* @__PURE__ */ new Set(["media", "nonce", "title", "blocking"]),
|
||
|
|
script: /* @__PURE__ */ new Set(["type", "textContent", "nonce", "blocking"]),
|
||
|
|
link: /* @__PURE__ */ new Set(["color", "crossorigin", "fetchpriority", "href", "hreflang", "imagesrcset", "imagesizes", "integrity", "media", "referrerpolicy", "rel", "sizes", "type"])
|
||
|
|
};
|
||
|
|
const BlockedLinkRels = /* @__PURE__ */ new Set(["canonical", "modulepreload", "prerender", "preload", "prefetch", "dns-prefetch", "preconnect", "manifest", "pingback"]);
|
||
|
|
const AsciiWhitespace = /[\t\n\f\r ]+/;
|
||
|
|
const SafeAttrName = /^[a-z][a-z0-9\-]*[a-z0-9]$/i;
|
||
|
|
const HtmlEntityHex = /&#x([0-9a-f]+);?/gi;
|
||
|
|
const HtmlEntityDec = /&#(\d+);?/g;
|
||
|
|
const HtmlEntityNamed = /&(tab|newline|colon|semi|lpar|rpar|sol|bsol|comma|period|excl|num|dollar|percnt|amp|apos|ast|plus|lt|gt|equals|quest|at|lsqb|rsqb|lcub|rcub|vert|hat|grave|tilde|nbsp);?/gi;
|
||
|
|
const ControlChars = /[\x00-\x20]+/g;
|
||
|
|
const NamedEntityMap = {
|
||
|
|
tab: " ",
|
||
|
|
newline: "\n",
|
||
|
|
colon: ":",
|
||
|
|
semi: ";",
|
||
|
|
lpar: "(",
|
||
|
|
rpar: ")",
|
||
|
|
sol: "/",
|
||
|
|
bsol: "\\",
|
||
|
|
comma: ",",
|
||
|
|
period: ".",
|
||
|
|
excl: "!",
|
||
|
|
num: "#",
|
||
|
|
dollar: "$",
|
||
|
|
percnt: "%",
|
||
|
|
amp: "&",
|
||
|
|
apos: "'",
|
||
|
|
ast: "*",
|
||
|
|
plus: "+",
|
||
|
|
lt: "<",
|
||
|
|
gt: ">",
|
||
|
|
equals: "=",
|
||
|
|
quest: "?",
|
||
|
|
at: "@",
|
||
|
|
lsqb: "[",
|
||
|
|
rsqb: "]",
|
||
|
|
lcub: "{",
|
||
|
|
rcub: "}",
|
||
|
|
vert: "|",
|
||
|
|
hat: "^",
|
||
|
|
grave: "`",
|
||
|
|
tilde: "~",
|
||
|
|
nbsp: "\xA0"
|
||
|
|
};
|
||
|
|
function safeFromCodePoint(codePoint) {
|
||
|
|
if (codePoint > 1114111 || codePoint < 0 || Number.isNaN(codePoint))
|
||
|
|
return "";
|
||
|
|
return String.fromCodePoint(codePoint);
|
||
|
|
}
|
||
|
|
function decodeHtmlEntities(str) {
|
||
|
|
return str.replace(HtmlEntityHex, (_, hex) => safeFromCodePoint(Number.parseInt(hex, 16))).replace(HtmlEntityDec, (_, dec) => safeFromCodePoint(Number(dec))).replace(HtmlEntityNamed, (_, name) => NamedEntityMap[name.toLowerCase()] || "");
|
||
|
|
}
|
||
|
|
function hasDangerousProtocol(url) {
|
||
|
|
const entityDecoded = decodeHtmlEntities(url);
|
||
|
|
const cleaned = entityDecoded.replace(ControlChars, "");
|
||
|
|
let decoded;
|
||
|
|
try {
|
||
|
|
decoded = decodeURIComponent(cleaned);
|
||
|
|
} catch {
|
||
|
|
decoded = cleaned;
|
||
|
|
}
|
||
|
|
const sanitized = decoded.replace(ControlChars, "");
|
||
|
|
const lower = sanitized.toLowerCase();
|
||
|
|
return lower.startsWith("javascript:") || lower.startsWith("data:") || lower.startsWith("vbscript:");
|
||
|
|
}
|
||
|
|
function stripProtoKeys(obj) {
|
||
|
|
if (Array.isArray(obj))
|
||
|
|
return obj.map(stripProtoKeys);
|
||
|
|
if (obj && typeof obj === "object") {
|
||
|
|
const clean = {};
|
||
|
|
for (const key of Object.keys(obj)) {
|
||
|
|
if (isUnsafeKey(key))
|
||
|
|
continue;
|
||
|
|
clean[key] = stripProtoKeys(obj[key]);
|
||
|
|
}
|
||
|
|
return clean;
|
||
|
|
}
|
||
|
|
return obj;
|
||
|
|
}
|
||
|
|
function acceptDataAttrs(value, allowId = true) {
|
||
|
|
return Object.fromEntries(
|
||
|
|
Object.entries(value || {}).filter(([key]) => (allowId && key === "id" || key.startsWith("data-")) && SafeAttrName.test(key))
|
||
|
|
);
|
||
|
|
}
|
||
|
|
function hasBlockedRel(rel) {
|
||
|
|
const tokens = rel.split(AsciiWhitespace);
|
||
|
|
return !tokens.some(Boolean) || tokens.some((token) => BlockedLinkRels.has(token.toLowerCase()));
|
||
|
|
}
|
||
|
|
function makeTagSafe(tag) {
|
||
|
|
let next = {};
|
||
|
|
const { tag: type, props: prev } = tag;
|
||
|
|
switch (type) {
|
||
|
|
// title: textContent is escaped in rendering (tagToString), no props needed
|
||
|
|
case "title":
|
||
|
|
break;
|
||
|
|
// virtual tags, not rendered to HTML — but sanitize to prevent injection if rendered
|
||
|
|
case "titleTemplate":
|
||
|
|
case "templateParams":
|
||
|
|
next = prev;
|
||
|
|
break;
|
||
|
|
case "htmlAttrs":
|
||
|
|
case "bodyAttrs":
|
||
|
|
WhitelistAttributes[type].forEach((attr) => {
|
||
|
|
if (prev[attr]) {
|
||
|
|
next[attr] = prev[attr];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
delete tag.innerHTML;
|
||
|
|
delete tag.textContent;
|
||
|
|
tag.props = { ...acceptDataAttrs(prev, false), ...next };
|
||
|
|
return !Object.keys(tag.props).length ? false : tag;
|
||
|
|
case "style":
|
||
|
|
next = acceptDataAttrs(prev);
|
||
|
|
WhitelistAttributes.style.forEach((key) => {
|
||
|
|
if (prev[key]) {
|
||
|
|
next[key] = prev[key];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
break;
|
||
|
|
// meta is safe, except for http-equiv
|
||
|
|
case "meta":
|
||
|
|
WhitelistAttributes.meta.forEach((key) => {
|
||
|
|
if (hasContent(prev[key])) {
|
||
|
|
next[key] = prev[key];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
break;
|
||
|
|
// link tags we block preloading, prerendering, prefetching, dns-prefetch, preconnect, manifest, etc
|
||
|
|
case "link":
|
||
|
|
WhitelistAttributes.link.forEach((key) => {
|
||
|
|
const val = prev[key];
|
||
|
|
if (!val) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (key === "rel" && (typeof val !== "string" || hasBlockedRel(val))) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (key === "href" || key === "imagesrcset") {
|
||
|
|
if (typeof val !== "string") {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const urls = key === "imagesrcset" ? val.split(",").map((s) => s.trim()) : [val];
|
||
|
|
if (urls.some((u) => hasDangerousProtocol(u))) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
next[key] = val;
|
||
|
|
} else if (val) {
|
||
|
|
next[key] = val;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
if (!next.href && !next.imagesrcset || !next.rel) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case "noscript":
|
||
|
|
WhitelistAttributes.noscript.forEach((key) => {
|
||
|
|
if (prev[key]) {
|
||
|
|
next[key] = prev[key];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
break;
|
||
|
|
// we only allow JSON in scripts
|
||
|
|
case "script":
|
||
|
|
if (!tag.textContent || typeof prev.type !== "string" || !prev.type.endsWith("json")) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const jsonVal = typeof tag.textContent === "string" ? JSON.parse(tag.textContent) : tag.textContent;
|
||
|
|
tag.textContent = JSON.stringify(stripProtoKeys(jsonVal), null, 0);
|
||
|
|
} catch {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
WhitelistAttributes.script.forEach((s) => {
|
||
|
|
if (s !== "textContent" && prev[s]) {
|
||
|
|
next[s] = prev[s];
|
||
|
|
}
|
||
|
|
});
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
delete tag.innerHTML;
|
||
|
|
if (type !== "title" && type !== "script") {
|
||
|
|
delete tag.textContent;
|
||
|
|
}
|
||
|
|
tag.props = { ...acceptDataAttrs(prev), ...next };
|
||
|
|
if (!Object.keys(tag.props).length && !tag.tag.endsWith("Attrs") && !tag.textContent) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return tag;
|
||
|
|
}
|
||
|
|
const SafeInputPlugin = (
|
||
|
|
/* @PURE */
|
||
|
|
defineHeadPlugin({
|
||
|
|
key: "safe",
|
||
|
|
hooks: {
|
||
|
|
"entries:normalize": (ctx) => {
|
||
|
|
if (ctx.entry.options?._safe) {
|
||
|
|
ctx.tags = ctx.tags.reduce((acc, tag) => {
|
||
|
|
const safeTag = makeTagSafe(tag);
|
||
|
|
if (safeTag)
|
||
|
|
acc.push(safeTag);
|
||
|
|
return acc;
|
||
|
|
}, []);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"tags:afterResolve": (ctx) => {
|
||
|
|
ctx.tags = ctx.tags.reduce((acc, tag) => {
|
||
|
|
if (!tag._safe || tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
|
||
|
|
acc.push(tag);
|
||
|
|
return acc;
|
||
|
|
}
|
||
|
|
const safeTag = makeTagSafe(tag);
|
||
|
|
if (safeTag)
|
||
|
|
acc.push(safeTag);
|
||
|
|
return acc;
|
||
|
|
}, []);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
export { FlatMetaPlugin as F, SafeInputPlugin as S, defineHeadPlugin as d };
|