Public: anyone can read this; the crew can change it.
A test pins where a tell says it was said: a `from` that is not this forge's page or app address is refused #7125
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7092-a-test-pins-where-a-tell-says-it-was-sai → main 1 commit fork 1 thread · 1 unresolved
1 file
changed +86
−0
test/tell-from.test.ts added +86
| @@ -0,0 +1,86 @@ | ||
| 1 | /** `from` on a tell is a place on this forge, or an app's own address from that app (#7092). */ | |
| 2 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 3 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createRepo } from "../src/actions/repos.ts"; | |
| 8 | import { tell } from "../src/actions/tell.ts"; | |
| 9 | import { listIssues } from "../src/core/issues.ts"; | |
| 10 | import type { Repo } from "../src/core/repos.ts"; | |
| 11 | import { createUser, type User } from "../src/core/users.ts"; | |
| 12 | ||
| 13 | let dir: string; | |
| 14 | let app: App; | |
| 15 | let alice: User; | |
| 16 | let repo: Repo; | |
| 17 | ||
| 18 | beforeAll(async () => { | |
| 19 | dir = await mkdtemp(join(tmpdir(), "forge-tellfrom2-")); | |
| 20 | app = createApp({ dataDir: dir, port: 0 }); | |
| 21 | const u = await createUser(app.db, { | |
| 22 | origin: "seed", | |
| 23 | trusted: true, | |
| 24 | username: "alice", | |
| 25 | email: "[email protected]", | |
| 26 | password: "a-good-passphrase", | |
| 27 | adminIfFirst: true, | |
| 28 | }); | |
| 29 | if (!u.ok) throw new Error(u.error); | |
| 30 | alice = u.value; | |
| 31 | const r = await createRepo(app, alice, { name: "notes", visibility: "public" }); | |
| 32 | if (!r.ok) throw new Error(String(r.error)); | |
| 33 | repo = r.value; | |
| 34 | }); | |
| 35 | ||
| 36 | afterAll(async () => { | |
| 37 | await rm(dir, { recursive: true, force: true }); | |
| 38 | }); | |
| 39 | ||
| 40 | const stored = () => listIssues(app.db, repo.id, {}).length; | |
| 41 | ||
| 42 | describe("tell from", () => { | |
| 43 | test("a page on this forge is kept on the issue", async () => { | |
| 44 | const t = await tell(app, alice, repo, "Page tell", 1, "/alice/notes/pulls/3"); | |
| 45 | expect(t.ok).toBe(true); | |
| 46 | if (!t.ok) return; | |
| 47 | expect(t.value.issue.body).toContain("Told from [/alice/notes/pulls/3](/alice/notes/pulls/3)"); | |
| 48 | }); | |
| 49 | ||
| 50 | test("another host, javascript:, or a control character is bad_from and stores nothing", async () => { | |
| 51 | const before = stored(); | |
| 52 | for (const from of [ | |
| 53 | "https://evil.example/alice/notes", | |
| 54 | "//evil.example/x", | |
| 55 | "javascript:alert(1)", | |
| 56 | "/alice/notes\nInjected", | |
| 57 | "/alice/notes\x00", | |
| 58 | "/alice/notes\x7f", | |
| 59 | ]) { | |
| 60 | for (const fromApp of [false, true]) { | |
| 61 | const t = await tell(app, alice, repo, "Nope", 1, from, fromApp); | |
| 62 | if (fromApp && from.startsWith("https://")) continue; // an app address, below | |
| 63 | expect(t.ok).toBe(false); | |
| 64 | if (!t.ok) expect(t.error).toBe("bad_from"); | |
| 65 | } | |
| 66 | } | |
| 67 | expect(stored()).toBe(before); | |
| 68 | }); | |
| 69 | ||
| 70 | test("an app's own address is accepted only when the tell comes from that app", async () => { | |
| 71 | const address = "https://notes--alice.apps.example"; | |
| 72 | const before = stored(); | |
| 73 | const web = await tell(app, alice, repo, "From the web", 1, address); | |
| 74 | expect(web.ok).toBe(false); | |
| 75 | if (!web.ok) expect(web.error).toBe("bad_from"); | |
| 76 | expect(stored()).toBe(before); | |
| 77 | const fromApp = await tell(app, alice, repo, "From the app", 1, address, true); | |
| 78 | expect(fromApp.ok).toBe(true); | |
| 79 | if (fromApp.ok) expect(fromApp.value.issue.body).toContain(`Told from [${address}]`); | |
| 80 | // Even from an app, only scheme and host: no path, no javascript:. | |
| 81 | for (const from of [`${address}/x`, "javascript:alert(1)", "https://a b.example"]) { | |
| 82 | const t = await tell(app, alice, repo, "Bad app", 1, from, true); | |
| 83 | expect(t.ok).toBe(false); | |
| 84 | } | |
| 85 | }); | |
| 86 | }); |
new above
offline
The call runs, then continue skips the assertion for https:// inputs with fromApp=true. If one were wrongly accepted it would be stored and never checked. Assert bad_from here too, since these have paths.