agent-sanitizer 2.66.1 → 2.66.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THREAT-MODEL.md +7 -3
- package/package.json +1 -1
- package/src/html.mjs +146 -36
- package/types/html.d.mts +1 -0
package/THREAT-MODEL.md
CHANGED
|
@@ -134,9 +134,13 @@ spliced, a `<script>` inside it reported, a URL inside it read by Layer 3. A
|
|
|
134
134
|
declarative shadow root (`shadowrootmode="open"`/`"closed"`) is reported too,
|
|
135
135
|
since one the parser fails to attach (a second root on one host, say) is an
|
|
136
136
|
ordinary inert template; its content is laid out in its host's box, so a flex
|
|
137
|
-
host blockifies it as it would a light-DOM child.
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
host blockifies it as it would a light-DOM child. An inherited hide on the host
|
|
138
|
+
(`visibility:hidden`, say) is judged through the root the parser attaches (the
|
|
139
|
+
first one, on a host `attachShadow` accepts), whose content inherits from the
|
|
140
|
+
host and never from the template's own attributes; a slot given light-DOM
|
|
141
|
+
children there renders them, so its fallback content is spliced. Slot
|
|
142
|
+
assignment is otherwise not modelled: a hidden `<slot>` splices only itself,
|
|
143
|
+
not the light-DOM children the browser renders through it. A `template` inside SVG or MathML (or an HTML
|
|
140
144
|
`template.x`) is not reported: it has no template content, so its children are
|
|
141
145
|
ordinary children and are scanned as such.
|
|
142
146
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.66.
|
|
3
|
+
"version": "2.66.2",
|
|
4
4
|
"description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
package/src/html.mjs
CHANGED
|
@@ -1680,6 +1680,100 @@ function layoutContainer(parent) {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
}
|
|
1682
1682
|
|
|
1683
|
+
// The elements `attachShadow` accepts as a host, besides a custom element.
|
|
1684
|
+
const SHADOW_HOST_TAGS = new Set([
|
|
1685
|
+
"article",
|
|
1686
|
+
"aside",
|
|
1687
|
+
"blockquote",
|
|
1688
|
+
"body",
|
|
1689
|
+
"div",
|
|
1690
|
+
"footer",
|
|
1691
|
+
"h1",
|
|
1692
|
+
"h2",
|
|
1693
|
+
"h3",
|
|
1694
|
+
"h4",
|
|
1695
|
+
"h5",
|
|
1696
|
+
"h6",
|
|
1697
|
+
"header",
|
|
1698
|
+
"main",
|
|
1699
|
+
"nav",
|
|
1700
|
+
"p",
|
|
1701
|
+
"section",
|
|
1702
|
+
"span",
|
|
1703
|
+
]);
|
|
1704
|
+
// Hyphenated names HTML reserves, so no custom element takes them.
|
|
1705
|
+
const RESERVED_CUSTOM_ELEMENT_NAMES = new Set([
|
|
1706
|
+
"annotation-xml",
|
|
1707
|
+
"color-profile",
|
|
1708
|
+
"font-face",
|
|
1709
|
+
"font-face-src",
|
|
1710
|
+
"font-face-uri",
|
|
1711
|
+
"font-face-format",
|
|
1712
|
+
"font-face-name",
|
|
1713
|
+
"missing-glyph",
|
|
1714
|
+
]);
|
|
1715
|
+
|
|
1716
|
+
// True for an ASCII valid custom element name (the tokenizer only opens a tag
|
|
1717
|
+
// on a letter, lowercased). The grammar also admits non-ASCII names; reading
|
|
1718
|
+
// one as no host leaves its shadow content inert, which only ever splices more
|
|
1719
|
+
// of a hidden host.
|
|
1720
|
+
/** @param {string} name @returns {boolean} */
|
|
1721
|
+
function isCustomElementName(name) {
|
|
1722
|
+
return (
|
|
1723
|
+
name.includes("-") &&
|
|
1724
|
+
!RESERVED_CUSTOM_ELEMENT_NAMES.has(name) &&
|
|
1725
|
+
[...name].every(
|
|
1726
|
+
(char) =>
|
|
1727
|
+
(char >= "a" && char <= "z") ||
|
|
1728
|
+
(char >= "0" && char <= "9") ||
|
|
1729
|
+
"-._".includes(char),
|
|
1730
|
+
)
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// The declarative shadow root the parser attaches to `host`: its first template
|
|
1735
|
+
// child with a valid `shadowrootmode`, when `attachShadow` accepts the host.
|
|
1736
|
+
// Every other template stays in the tree as an ordinary, inert one.
|
|
1737
|
+
/** @param {any} host a hast element @returns {any} the attached template */
|
|
1738
|
+
function attachedShadowTemplate(host) {
|
|
1739
|
+
if (!SHADOW_HOST_TAGS.has(host.tagName) && !isCustomElementName(host.tagName))
|
|
1740
|
+
return undefined;
|
|
1741
|
+
return host.children.find(
|
|
1742
|
+
(/** @type {any} */ child) =>
|
|
1743
|
+
child.content !== undefined && !isInertTemplate(child),
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// The slot names `host`'s light children are assigned to: an element's `slot`
|
|
1748
|
+
// attribute, or the default slot for a text node or an element without one.
|
|
1749
|
+
/** @param {any} host @param {any} template @returns {Set<string>} */
|
|
1750
|
+
function assignedSlotNames(host, template) {
|
|
1751
|
+
const names = new Set();
|
|
1752
|
+
for (const child of host.children) {
|
|
1753
|
+
if (child === template) continue;
|
|
1754
|
+
if (child.type === "text") names.add("");
|
|
1755
|
+
else if (child.type === "element")
|
|
1756
|
+
names.add(String(child.properties.slot ?? ""));
|
|
1757
|
+
}
|
|
1758
|
+
return names;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// `node`'s children in render order, as runs no splice may span. A declarative
|
|
1762
|
+
// shadow root attached to `node` replaces its template with the content's
|
|
1763
|
+
// children, laid out in `node` and inheriting from it: the parser removes the
|
|
1764
|
+
// template, so its own attributes style nothing. That run carries the template.
|
|
1765
|
+
/** @param {any} node @returns {Array<[any[], any]>} */
|
|
1766
|
+
function renderedRuns(node) {
|
|
1767
|
+
const template = attachedShadowTemplate(node);
|
|
1768
|
+
if (!template) return [[node.children, undefined]];
|
|
1769
|
+
const at = node.children.indexOf(template);
|
|
1770
|
+
return [
|
|
1771
|
+
[node.children.slice(0, at), undefined],
|
|
1772
|
+
[template.content.children, template],
|
|
1773
|
+
[node.children.slice(at + 1), undefined],
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1683
1777
|
// True when `node` lays out as a non-replaced INLINE box. A flex/grid layout
|
|
1684
1778
|
// container (see {@link layoutContainer}) blockifies it.
|
|
1685
1779
|
/** @param {any} node @param {any} [parent] @returns {boolean} */
|
|
@@ -1780,7 +1874,8 @@ function keepsHide(kind, cascade, inlineBox, color) {
|
|
|
1780
1874
|
return keeps;
|
|
1781
1875
|
}
|
|
1782
1876
|
|
|
1783
|
-
|
|
1877
|
+
// `slots`: the assigned slot names of the shadow tree a node sits in, if any.
|
|
1878
|
+
/** @typedef {{ kinds: Set<InheritedHide>, color: ColorState, slots: Set<string> | undefined }} RegionState */
|
|
1784
1879
|
// `neutral`: nothing to see either way; `mixed`: hidden and visible content.
|
|
1785
1880
|
/** @typedef {"hidden" | "neutral" | "visible" | "mixed"} RegionVerdict */
|
|
1786
1881
|
|
|
@@ -1795,11 +1890,17 @@ function regionVerdicts(root, kinds) {
|
|
|
1795
1890
|
const stack = [];
|
|
1796
1891
|
/** @param {any} node @param {RegionState} state */
|
|
1797
1892
|
const pushChildren = (node, state) => {
|
|
1798
|
-
for (const
|
|
1893
|
+
for (const [children, template] of renderedRuns(node)) {
|
|
1894
|
+
const runState = template
|
|
1895
|
+
? { ...state, slots: assignedSlotNames(node, template) }
|
|
1896
|
+
: state;
|
|
1897
|
+
for (const child of children) stack.push([child, node, runState]);
|
|
1898
|
+
}
|
|
1799
1899
|
};
|
|
1800
1900
|
pushChildren(root, {
|
|
1801
1901
|
kinds,
|
|
1802
1902
|
color: colorState(elementCascade(root), null),
|
|
1903
|
+
slots: undefined,
|
|
1803
1904
|
});
|
|
1804
1905
|
const order = [root];
|
|
1805
1906
|
while (stack.length > 0) {
|
|
@@ -1821,7 +1922,11 @@ function regionVerdicts(root, kinds) {
|
|
|
1821
1922
|
// Only an element's own context-free hides are ever added on the way
|
|
1822
1923
|
// down, so a region never hides more than its elements would alone.
|
|
1823
1924
|
const own = elementHides(node, parent);
|
|
1824
|
-
|
|
1925
|
+
// A slot given light children renders them, never its fallback content.
|
|
1926
|
+
const unrenderedFallback =
|
|
1927
|
+
node.tagName === "slot" &&
|
|
1928
|
+
state.slots?.has(String(node.properties.name ?? ""));
|
|
1929
|
+
if (own.has("box") || unrenderedFallback) {
|
|
1825
1930
|
verdicts.set(node, "hidden");
|
|
1826
1931
|
continue;
|
|
1827
1932
|
}
|
|
@@ -1834,7 +1939,7 @@ function regionVerdicts(root, kinds) {
|
|
|
1834
1939
|
if (REPLACED_ELEMENTS.has(node.tagName))
|
|
1835
1940
|
verdicts.set(node, inForce.has("visibility") ? "hidden" : "visible");
|
|
1836
1941
|
else if (inForce.size === 0) verdicts.set(node, "visible");
|
|
1837
|
-
else pushChildren(node, { kinds: inForce, color });
|
|
1942
|
+
else pushChildren(node, { kinds: inForce, color, slots: state.slots });
|
|
1838
1943
|
}
|
|
1839
1944
|
}
|
|
1840
1945
|
for (let i = order.length - 1; i >= 0; i--) {
|
|
@@ -1842,7 +1947,7 @@ function regionVerdicts(root, kinds) {
|
|
|
1842
1947
|
if (verdicts.has(node)) continue;
|
|
1843
1948
|
let hidden = false;
|
|
1844
1949
|
let shown = false;
|
|
1845
|
-
for (const child of node.children) {
|
|
1950
|
+
for (const child of renderedRuns(node).flatMap(([children]) => children)) {
|
|
1846
1951
|
const verdict = verdicts.get(child);
|
|
1847
1952
|
if (verdict === "hidden") hidden = true;
|
|
1848
1953
|
else if (verdict === "visible" || verdict === "mixed") shown = true;
|
|
@@ -1888,38 +1993,43 @@ function inheritedRegionSplice(root, kinds) {
|
|
|
1888
1993
|
trailingComments = [];
|
|
1889
1994
|
trailingElements = [];
|
|
1890
1995
|
};
|
|
1891
|
-
for (const
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
position
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1996
|
+
for (const [children, template] of renderedRuns(node)) {
|
|
1997
|
+
closeRun();
|
|
1998
|
+
// The template the parser removed is left standing, not spliced.
|
|
1999
|
+
if (template) mixed.push(template);
|
|
2000
|
+
for (const child of children) {
|
|
2001
|
+
const position = child.position;
|
|
2002
|
+
if (
|
|
2003
|
+
!position ||
|
|
2004
|
+
position.start.offset < lastEnd ||
|
|
2005
|
+
position.end.offset > node.position.end.offset
|
|
2006
|
+
)
|
|
2007
|
+
return null;
|
|
2008
|
+
lastEnd = position.end.offset;
|
|
2009
|
+
const verdict = verdicts.get(child);
|
|
2010
|
+
if (verdict === "hidden") {
|
|
2011
|
+
if (runStart === -1) {
|
|
2012
|
+
closeRun(); // what precedes the run stays outside it
|
|
2013
|
+
runStart = position.start.offset;
|
|
2014
|
+
}
|
|
2015
|
+
runEnd = position.end.offset;
|
|
2016
|
+
trailingComments = [];
|
|
2017
|
+
trailingElements = [];
|
|
2018
|
+
} else if (verdict === "mixed") {
|
|
2019
|
+
closeRun();
|
|
2020
|
+
stack.push(child);
|
|
2021
|
+
} else if (verdict === "visible") {
|
|
2022
|
+
closeRun();
|
|
2023
|
+
rescan.push([child, node]);
|
|
2024
|
+
} else if (child.type === "comment") {
|
|
2025
|
+
trailingComments.push({
|
|
2026
|
+
start: position.start.offset,
|
|
2027
|
+
end: position.end.offset,
|
|
2028
|
+
kind: "comment",
|
|
2029
|
+
});
|
|
2030
|
+
} else if (child.type === "element") {
|
|
2031
|
+
trailingElements.push([child, node]);
|
|
1905
2032
|
}
|
|
1906
|
-
runEnd = position.end.offset;
|
|
1907
|
-
trailingComments = [];
|
|
1908
|
-
trailingElements = [];
|
|
1909
|
-
} else if (verdict === "mixed") {
|
|
1910
|
-
closeRun();
|
|
1911
|
-
stack.push(child);
|
|
1912
|
-
} else if (verdict === "visible") {
|
|
1913
|
-
closeRun();
|
|
1914
|
-
rescan.push([child, node]);
|
|
1915
|
-
} else if (child.type === "comment") {
|
|
1916
|
-
trailingComments.push({
|
|
1917
|
-
start: position.start.offset,
|
|
1918
|
-
end: position.end.offset,
|
|
1919
|
-
kind: "comment",
|
|
1920
|
-
});
|
|
1921
|
-
} else if (child.type === "element") {
|
|
1922
|
-
trailingElements.push([child, node]);
|
|
1923
2033
|
}
|
|
1924
2034
|
}
|
|
1925
2035
|
closeRun();
|
package/types/html.d.mts
CHANGED
|
@@ -195,6 +195,7 @@ export type ColorState = {
|
|
|
195
195
|
export type RegionState = {
|
|
196
196
|
kinds: Set<InheritedHide>;
|
|
197
197
|
color: ColorState;
|
|
198
|
+
slots: Set<string> | undefined;
|
|
198
199
|
};
|
|
199
200
|
export type RegionVerdict = "hidden" | "neutral" | "visible" | "mixed";
|
|
200
201
|
export type CollectedUrl = {
|