tinyrelayfile
tinyrelay/scripts/qa/ux-http-fuzz.mjs/

scripts/qa/ux-http-fuzz.mjs raw
1#!/usr/bin/env node
2// Read-only HTTP fuzzing for the browser-facing hosted-content viewers.
3import { readFile, mkdir, writeFile } from "node:fs/promises";
4import { dirname, resolve } from "node:path";
5
6const args = process.argv.slice(2);
7const option = (key, fallback) => args.includes(key) ? args[args.indexOf(key) + 1] : fallback;
8const manifestPath = resolve(option("--manifest", "output/playwright/ux/fixture.json"));
9const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
10const base = (option("--relay", manifest.relay)).replace(/\/$/, "");
11const out = resolve(option("--out", "output/playwright/ux/http-fuzz.json"));
12const timeoutMs = Number(option("--timeout-ms", "5000"));
13const unusual = " café/路径 #hash ?query %percent &amp; <script>alert(1)</script>\u0000";
14const prefixes = ["", "/r/main"];
15const cases = [];
16const add = (name, path, query = "") => { for (const prefix of prefixes) cases.push({ name: `${prefix || "root"} ${name}`, url: `${base}${prefix}${path}${query}` }); };
17
18for (const q of ["", "?limit=-1&cursor=%00", `?q=${encodeURIComponent(unusual)}&limit=999999`, "?owner=%00&ref=refs%2Fheads%2Fmain&path=%2F..%2F"])
19 add("search", "/search" + q);
20for (const q of ["", `?owner=${encodeURIComponent(unusual)}&repo=${encodeURIComponent(unusual)}&ref=%00&path=${encodeURIComponent(unusual)}`, "?owner=%00&repo=%00&limit=NaN&cursor=%00"])
21 add("repo", "/repo" + q);
22for (const q of ["", `?hash=${encodeURIComponent(unusual)}&limit=-1&cursor=%00`, "?sha=%00&hash=not-a-sha"])
23 add("file", "/file" + q);
24for (const id of ["", "not-an-event", "0".repeat(64), unusual, manifest.notes[0].id]) add("event", "/e/" + encodeURIComponent(id));
25
26const results = [];
27const duplicateIDs = html => [...html.matchAll(/\bid=["']([^"']+)["']/gi)].map(m => m[1]).filter((id, i, all) => all.indexOf(id) !== i);
28async function request(item) {
29 const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs);
30 const started = performance.now();
31 try {
32 const response = await fetch(item.url, { method: "GET", redirect: "manual", signal: controller.signal });
33 const body = await response.text();
34 const duplicate = duplicateIDs(body);
35 const injected = /<script\s*>\s*alert\s*\(/i.test(body) || body.includes("<script>alert(1)</script>");
36 return { ...item, status: response.status, ms: Math.round(performance.now() - started), bytes: body.length, ok: response.status < 500 && !injected && duplicate.length === 0, checks: { no5xx: response.status < 500, noScriptInjection: !injected, noDuplicateIDs: duplicate.length === 0 }, ...(duplicate.length ? { duplicateIDs: duplicate } : {}) };
37 } catch (error) { return { ...item, status: 0, ms: Math.round(performance.now() - started), ok: false, error: error.name === "AbortError" ? "timeout" : error.message }; }
38 finally { clearTimeout(timer); }
39}
40for (const item of cases) results.push(await request(item));
41
42const valid = [];
43const validGet = async (name, path, expected) => {
44 const item = { name: `valid ${name}`, url: base + path }; const row = await request(item);
45 const body = row.status ? await (await fetch(item.url)).text() : "";
46 row.checks = { ...row.checks, expected: body.includes(expected), noClassAttributes: !/\bclass\s*=/i.test(body) };
47 row.ok = row.ok && row.checks.expected && row.checks.noClassAttributes;
48 valid.push(row);
49};
50const note = manifest.notes[0], article = manifest.articles[0], file = manifest.files.find(f => f.label === "markdown");
51await validGet("note", `/e/${note.id}`, note.id);
52await validGet("article", "/a/ux-long-article", "A long article");
53await validGet("articles", "/articles", "A long article");
54await validGet("files", "/files", "Files");
55await validGet("file", `/file?hash=${file.sha256}`, file.sha256);
56await validGet("repos", "/repos", manifest.repository.id);
57await validGet("repo home", `/repo?owner=${manifest.owner}&repo=${manifest.repository.id}`, manifest.repository.id);
58await validGet("repo tree", `/repo?owner=${manifest.owner}&repo=${manifest.repository.id}&view=tree&ref=refs%2Fheads%2Fmain`, "README UX fixture.md");
59await validGet("repo file", `/repo?owner=${manifest.owner}&repo=${manifest.repository.id}&view=file&ref=refs%2Fheads%2Fmain&path=${encodeURIComponent("nested dir/Unicode café/emoji 🧪.md")}`, "unicode path content");
60
61const report = { schema: 1, fixture: manifest.fixture, relay: base, generatedAt: new Date().toISOString(), cases: cases.length, fuzz: results.length, validCases: valid.length, passed: [...results, ...valid].filter(r => r.ok).length, failed: [...results, ...valid].filter(r => !r.ok).length, results, valid };
62await mkdir(dirname(out), { recursive: true }); await writeFile(out, JSON.stringify(report, null, 2) + "\n");
63console.log(JSON.stringify({ ok: report.failed === 0, cases: report.cases, valid: report.validCases, passed: report.passed, failed: report.failed, report: out }));
64process.exitCode = report.failed ? 1 : 0;

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs