Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 12 additions & 1 deletion apps/pwa/src/lib/moshpit-name.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@ export const RESERVED_TLDS = new Set([
]);

/** A TLD label: lowercase letters, digits and dashes; no leading/trailing dash. */
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
// Letters and digits only — no dashes, though DNS would allow them.
//
// A dash is the cheapest way to mint a near-miss of a name someone else holds.
// `.crypto` and `.cryp-to` read as the same ending at a glance and sort next to
// each other, so allowing dashes turns every claimed ending into a family of
// look-alikes worth squatting. The namespace is one level deep and first come
// first served, which makes that the whole attack: there is no second level to
// retreat to and no dispute process to appeal it.
//
// The cost is real names nobody can have — `lazy-loaded` cannot be an ending.
// That is the trade, and it is cheaper than policing look-alikes forever.
const LABEL = /^[a-z0-9]{1,63}$/;

/** A hostname label. Unlike a TLD, an all-numeric label is valid. */
export function normalizeLabel(input) {
Expand Down
21 changes: 20 additions & 1 deletion apps/pwa/test/moshpit-name.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test("normalizeTld accepts what people actually type", () => {
assert.equal(normalizeTld("eggs"), "eggs");
assert.equal(normalizeTld(".eggs"), "eggs");
assert.equal(normalizeTld(" .EGGS "), "eggs");
assert.equal(normalizeTld("web3-agents"), "web3-agents");
assert.equal(normalizeTld("web3agents"), "web3agents");
});

test("normalizeTld rejects what could never be a TLD", () => {
Expand Down Expand Up @@ -125,3 +125,22 @@ test("all-numeric endings", async (t) => {
assert.equal(parseMoshpitName("0.0"), null);
});
});

test("dashes are not part of a Moshpit name", () => {
// A dash is the cheapest way to mint a near-miss of an ending someone else
// holds — `.cryp-to` beside `.crypto` — and a namespace one level deep with
// no dispute process has nowhere to put the argument.
assert.equal(normalizeTld("lazy-loaded"), null);
assert.equal(normalizeTld("cryp-to"), null);
assert.equal(normalizeLabel("register-me"), null);
assert.equal(normalizeLabel("-leading"), null);
assert.equal(normalizeLabel("trailing-"), null);

// Unchanged either side of the dot.
assert.equal(normalizeTld("oranges"), "oranges");
assert.equal(normalizeTld("420"), "420");
assert.equal(normalizeLabel("california"), "california");
assert.equal(normalizeLabel("123"), "123");
assert.equal(normalizeLabel("a".repeat(63)), "a".repeat(63));
assert.equal(normalizeLabel("a".repeat(64)), null);
});
8 changes: 7 additions & 1 deletion src/dns.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,13 @@ export function parseRegistryName(hostname) {
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return null;
const parts = host.split(".");
if (parts.length !== 2) return null;
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
// Letters and digits only, matching the registry. A dash is the cheapest way
// to mint a look-alike of an ending someone else holds, and in a namespace
// one level deep and first come first served there is nowhere to retreat to.
// Keeping the rule here identical to the registry's matters more than the
// rule itself: a name this bridge accepts and the registry rejects resolves
// to a page that says it does not exist.
const LABEL = /^[a-z0-9]{1,63}$/;
const [label, tld] = parts;
if (!LABEL.test(label) || !LABEL.test(tld)) return null;
return { label, tld };
Expand Down
12 changes: 12 additions & 0 deletions test/dns.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,15 @@ test("answerFor hands the bridge a bare address, not the stored target", async (
});
assert.equal(address, "2606:4700:4700::1111");
});

test("dashes are not part of a Moshpit name", () => {
// Cheap look-alikes of an ending someone already holds. The namespace is one
// level deep and first come first served, so `.cryp-to` next to `.crypto` has
// nowhere to be appealed to.
assert.equal(parseRegistryName("blue.lazy-loaded"), null);
assert.equal(parseRegistryName("register-me.eggs"), null);
assert.equal(parseRegistryName("a-b.c-d"), null);
// Still fine either side of the dot.
assert.deepEqual(parseRegistryName("california.oranges"), { label: "california", tld: "oranges" });
assert.deepEqual(parseRegistryName("123.420"), { label: "123", tld: "420" });
});
Loading