Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pipelines/lwe/generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
const titleSuffix = c.title;
const stat = spec.buddyStat || c.navHint;
const pets = spec.petsLine || "Learning With Errors";
const favFill = c.accent.replace("#", "%23");
const favFill = c.accent.replaceAll("#", "%23");
const favFont = c.glyphFont ? c.glyphFont.replace(/"/g, "'") : "Trebuchet MS,sans-serif";
const askScript = (spec.hasAsk ?? c.hasAsk) ? `<script src="/lwe/ask.js" defer></script>\n` : "";
const demoJs = spec.demoJs ? `<script>\n${spec.demoJs}\n</script>\n` : "";
Expand Down Expand Up @@ -192,7 +192,7 @@
if (i === -1 || j === -1) { console.log(` · ${file}: markers not found, skipped (${start})`); return false; }
const next = src.slice(0, i + start.length) + "\n" + content + "\n" + src.slice(j);
writeFileSync(path, next);
console.log(` · ${file}: rewrote ${start.replace(/<!--|-->|\/\//g, "").trim()} region`);
console.log(` · ${file}: rewrote ${start.replace(/<!--|--!?>|\/\//g, "").trim()} region`);
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions scripts/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ async function checkInvariants() {
const routeKeys = [...routesBlock.matchAll(/\[\s*"([^"]+)"/g)].map((m) => m[1]);
const allowBlock = (wrangler.match(/"run_worker_first"\s*:\s*\[([\s\S]*?)\]/) || [,""])[1];
const allow = [...allowBlock.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
const globRe = (g) => new RegExp("^" + g.replace(/[.]/g, "\\$&").replace(/\*/g, ".*") + "$");
const globRe = (g) => new RegExp("^" + g.replace(/[\\.+?^${}()|[\]]/g, "\\$&").replace(/\*/g, ".*") + "$");
const covered = (p) => allow.includes(p) || allow.some((a) => a.includes("*") && globRe(a).test(p));
for (const k of routeKeys) if (!covered(k)) hard.push(`ROUTES key ${k} is not in wrangler run_worker_first (route would silently serve static)`);

Expand Down Expand Up @@ -1309,7 +1309,7 @@ for (const [file, srcPath, marker] of SHELLS) {
// the build is allowed past the six shells + luna.css (hard rule 3).
{
const hash8 = (buf) => createHash("sha256").update(buf).digest("hex").slice(0, 8);
const esc = (s) => s.replace(/[/.]/g, "\\$&");
const esc = (s) => s.replace(/[\\/.*+?^${}()|[\]]/g, "\\$&");
await mkdir(`${OUT}/www/a`, { recursive: true });

const ASSETS = [
Expand Down
27 changes: 27 additions & 0 deletions scripts/contract-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5016,6 +5016,33 @@ test("the default scan is unchanged — every phase still runs", async () => {
assert.deepEqual(lensObservationSummary({}).phases, { page: true, discovery: true, botViews: true });
});

// ── tag stripping: the closers HTML allows and a regex forgets ───────────

// `</script >` is a legal end tag and `--!>` legally closes a comment, so a stripper
// spelled `</script>` or `-->` hands the body straight through. Neither one is an
// injection here (lensMarkdown returns JSON that reaches the DOM through esc(), and an
// excerpt is stored escaped), so what it costs is fidelity: script source lands in a
// markdown render, and minified JS lands in a webmention excerpt that a human reads.
test("a spaced end tag and an --!> comment close their elements", async () => {
const { lensMarkdown } = await import("../www/_worker.js/lens.js");

const md = lensMarkdown("<body><p>keep</p><script >var leak=1;</script >tail</body>", "https://x/");
assert.ok(!md.includes("leak"), "a spaced </script > must close the script");
assert.ok(md.includes("keep") && md.includes("tail"), "the surrounding prose survives");

assert.ok(!lensMarkdown("<body><!-- gone --!><p>after</p></body>", "https://x/").includes("gone"),
"--!> closes a comment");

// <head> must not swallow a page that opens with <header>, which is what a
// missing word boundary buys: the strip runs to the next literal </head>.
assert.ok(lensMarkdown("<body><header><p>nav</p></header><p>body</p></body>", "https://x/").includes("body"),
"<header> is not <head>");

const { contentOf } = await import("../www/_worker.js/webmention-send.js");
assert.ok(!contentOf("<html><body><script >var s=1;</script >real</body></html>").includes("var s"),
"outbound link extraction skips script bodies closed with a space");
});

// ── lens parse budget: the CPU-bound half ────────────────────────────────

test("the parse is capped, and a prefix parse says so instead of under-reporting", async () => {
Expand Down
28 changes: 23 additions & 5 deletions www/_worker.js/lens.js
Original file line number Diff line number Diff line change
Expand Up @@ -2673,14 +2673,32 @@
return lensDecode(s).replace(/\s+/g, " ").trim();
}

// One pass of /<[^>]+>/g is ALREADY a fixpoint, and it is worth writing down why so
// nobody re-derives it: the match runs from a `<` to the next `>`, so any `<` that
// survives a pass has no `>` after it anywhere, and a second pass can never match.
// The classic re-formation bug (a pass eating an inner tag and closing an outer one
// out of the leftovers) needs a NARROWER pattern, like /<script>/g, to bite.
//
// The loop is here for the two readers who cannot see that argument: CodeQL, which
// flags every single-pass multi-character strip as js/incomplete-multi-character-
// sanitization, and the next person to swap this regex for a narrower one, where the
// proof stops holding and the loop starts earning its keep. It costs one extra scan
// that finds nothing. Bounded anyway, because this runs on third-party pages up to
// 2MB and an unbounded fixpoint over someone else's bytes is their budget to spend.
function lensStripTags(h, repl = "") {
let out = String(h), prev = null, guard = 0;
while (out !== prev && guard++ < 8) { prev = out; out = out.replace(/<[^>]+>/g, repl); }
return out;
}

// best-effort, dependency-free HTML→Markdown — roughly what a basic LLM
// scraper ingests. High-fidelity Readability/Turndown is a deliberate v2.
export function lensMarkdown(html, baseUrl) {
let s = html;
const b = s.match(/<body[^>]*>([\s\S]*)<\/body>/i); if (b) s = b[1];
s = s.replace(/<!--[\s\S]*?-->/g, "");
s = s.replace(/<(script|style|noscript|template|svg|head|nav|footer|aside)[\s\S]*?<\/\1>/gi, "");
s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (m, i) => "\n\n```\n" + lensDecode(i.replace(/<[^>]+>/g, "")).replace(/\n+$/g, "") + "\n```\n\n");
s = s.replace(/<!--[\s\S]*?(?:-->|--!>)/g, "");
Comment thread
oddharsh marked this conversation as resolved.
Dismissed
s = s.replace(/<(script|style|noscript|template|svg|head|nav|footer|aside)\b[\s\S]*?<\/\1\s*>/gi, "");

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Same fixpoint loop, plus the end tag is <\/\1\b[^>]*> now so </script bar> closes the element too.

🤖 Addressed by Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same correction as the sibling thread: the end tag fix (<\/\1\b[^>]*>) shipped and stands, but the fixpoint loop I mentioned came back out in 5de4d4e after measuring it. This one reopened as alert 69 and wants a dismissal.

🤖 Addressed by Claude Code

s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (m, i) => "\n\n```\n" + lensDecode(lensStripTags(i)).replace(/\n+$/g, "") + "\n```\n\n");
s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (m, i) => "`" + lensStripInline(i).trim() + "`");
s = s.replace(/<br\s*\/?>/gi, "\n");
s = s.replace(/<hr\s*\/?>/gi, "\n\n---\n\n");
Expand All @@ -2692,13 +2710,13 @@
s = s.replace(/<(strong|b)\b[^>]*>([\s\S]*?)<\/\1>/gi, (m, t, i) => "**" + lensStripInline(i).trim() + "**");
s = s.replace(/<(em|i)\b[^>]*>([\s\S]*?)<\/\1>/gi, (m, t, i) => "*" + lensStripInline(i).trim() + "*");
s = s.replace(/<\/(p|div|section|article|header|main|ul|ol|table|tr|h[1-6])>/gi, "\n\n");
s = s.replace(/<[^>]+>/g, "");
s = lensStripTags(s);
s = lensDecode(s);
s = s.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ");
return s.trim();
}

export function lensStripInline(h) { return lensDecode(String(h).replace(/<[^>]+>/g, " ")).replace(/\s+/g, " "); }
export function lensStripInline(h) { return lensDecode(lensStripTags(h, " ")).replace(/\s+/g, " "); }

export function lensTagAttr(tag, name) { const m = String(tag).match(new RegExp(name + "\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))", "i")); return m ? (m[2] ?? m[3] ?? m[4] ?? "") : ""; }

Expand Down
8 changes: 4 additions & 4 deletions www/_worker.js/webmention-send.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@
return String(html)
.replace(/<!-- axp:shell -->[\s\S]*?<!-- \/axp:shell -->/g, " ")
.replace(/<!-- axp:desktop -->[\s\S]*?<!-- \/axp:desktop -->/g, " ")
.replace(/<head\b[\s\S]*?<\/head>/i, " ")
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<!--[\s\S]*?-->/g, " ");
.replace(/<head\b[\s\S]*?<\/head\s*>/i, " ")
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, " ")
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, " ")
.replace(/<!--[\s\S]*?(?:-->|--!>)/g, " ");
}

// Every external link in the page's own content, deduped, minus my own profiles
Expand Down
10 changes: 5 additions & 5 deletions www/_worker.js/webmention.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,15 +310,15 @@
// about my page — far more useful than the post's opening line.
function excerptAround(html, target) {
const stripped = String(html)
.replace(/<script\b[\s\S]*?<\/script>/gi, " ")
.replace(/<style\b[\s\S]*?<\/style>/gi, " ")
.replace(/<script\b[\s\S]*?<\/script\s*>/gi, " ")
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
.replace(/<style\b[\s\S]*?<\/style\s*>/gi, " ")
// <svg> matters as much as <script> here: an inline icon carries a d="M9.64a1.998
// 2 0 0 0 2.83 0l1.25-1.25…" path, and a page like a GitHub gist is full of them.
// Left in, that geometry is what lands in the excerpt and makes a real mention
// read like spam.
.replace(/<svg\b[\s\S]*?<\/svg>/gi, " ")
.replace(/<noscript\b[\s\S]*?<\/noscript>/gi, " ")
.replace(/<!--[\s\S]*?-->/g, " ");
.replace(/<svg\b[\s\S]*?<\/svg\s*>/gi, " ")
.replace(/<noscript\b[\s\S]*?<\/noscript\s*>/gi, " ")
.replace(/<!--[\s\S]*?(?:-->|--!>)/g, " ");
const e = firstMatch(stripped, /class="[^"]*\be-content\b[^"]*"[^>]*>([\s\S]{0,1200})</i);
const hay = e || stripped;
const idx = hay.indexOf(target);
Expand Down