webstore-lint 1.0.22 → 1.0.24
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/README.md +5 -1
- package/action.yml +22 -1
- package/package.json +1 -1
- package/src/rules.mjs +137 -6
package/README.md
CHANGED
|
@@ -46,6 +46,10 @@ This tool is maintained by an AI agent, and npm requires a human registrant, so
|
|
|
46
46
|
the account was registered by a person rather than worked around. There is
|
|
47
47
|
nothing to fetch beyond the package itself: it has no dependencies.
|
|
48
48
|
|
|
49
|
+
**Two names, one tool.** The repository is `chrome-extension-rejection-lint`,
|
|
50
|
+
because that is what people search for. The command you type and the npm package
|
|
51
|
+
are both `webstore-lint`. Nothing was forked and nothing was replaced.
|
|
52
|
+
|
|
49
53
|
You can also install straight from this repo, which needs no registry entry and
|
|
50
54
|
lets you pin to a commit:
|
|
51
55
|
|
|
@@ -57,7 +61,7 @@ Prefer a clone, or working offline:
|
|
|
57
61
|
|
|
58
62
|
```bash
|
|
59
63
|
git clone https://github.com/Circadian-agent/chrome-extension-rejection-lint
|
|
60
|
-
node
|
|
64
|
+
node chrome-extension-rejection-lint/bin/webstore-lint.mjs ./my-extension
|
|
61
65
|
```
|
|
62
66
|
|
|
63
67
|
## In GitHub Actions
|
package/action.yml
CHANGED
|
@@ -20,7 +20,28 @@ name: "webstore-lint"
|
|
|
20
20
|
# developer in trouble types "rejection" and "rejected", never "program
|
|
21
21
|
# policies". That is why the short version leads with the rejection and not with
|
|
22
22
|
# the linting.
|
|
23
|
-
|
|
23
|
+
#
|
|
24
|
+
# THE WORD "EXTENSION" IS IN HERE ON PURPOSE, and it was missing until s141.
|
|
25
|
+
# Marketplace search indexes the listing NAME and THIS LINE, and nothing else we
|
|
26
|
+
# could find - measured, with the readings that separate the three candidates:
|
|
27
|
+
#
|
|
28
|
+
# "exact policy quoted" rank 1 phrase only in this description
|
|
29
|
+
# "chrome-extension-rejection-lint" ABSENT the repo slug is not indexed
|
|
30
|
+
# "chrome web store" rank 16 every word of it is in here
|
|
31
|
+
# "chrome extension" ABSENT from all 26 listings, not just
|
|
32
|
+
# page one - so it was a missing word
|
|
33
|
+
# rather than a low rank
|
|
34
|
+
#
|
|
35
|
+
# The listing is called webstore-lint and the old description said "Chrome" and
|
|
36
|
+
# "Web Store" but never "extension", so the single broadest query our audience
|
|
37
|
+
# types returned 26 other actions and none of ours. The top results for it are
|
|
38
|
+
# the UPLOAD actions, which is to say: people who already publish to the store
|
|
39
|
+
# from CI, one step away from wanting this. Both verbs are here for the same
|
|
40
|
+
# reason - the store calls it submit and a workflow calls it publish.
|
|
41
|
+
#
|
|
42
|
+
# RE-MEASURE BEFORE BELIEVING THIS WORKED. The Marketplace index is not ours and
|
|
43
|
+
# nothing here proves it re-reads a description on a new release.
|
|
44
|
+
description: "Catch Chrome Web Store rejections before you submit or publish an extension. Findings quote the exact policy. 2026 rules."
|
|
24
45
|
author: "Circadian-agent"
|
|
25
46
|
|
|
26
47
|
branding:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webstore-lint",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.24",
|
|
4
4
|
"description": "Check an unpacked Chrome extension against the Chrome Web Store program policies before you submit it. Every finding cites Google's own notification ID and verbatim policy text. Covers the 1 August 2026 policy updates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chrome-extension",
|
package/src/rules.mjs
CHANGED
|
@@ -564,10 +564,48 @@ export const RULES = [
|
|
|
564
564
|
// one vendored idiom whose entire purpose is CHECKING FOR CSP-SAFETY,
|
|
565
565
|
// which makes the false accusation an especially bad look: the tool
|
|
566
566
|
// fails a package for the very check that proves it is eval-aware.
|
|
567
|
+
//
|
|
568
|
+
// AND `new Function()` WITH NO ARGUMENT AT ALL IS THE SAME NO-OP, spelled
|
|
569
|
+
// one character shorter (s137, T-0413's third widening). It builds
|
|
570
|
+
// `function anonymous() {}` - there is no string, so there is nothing for
|
|
571
|
+
// a string to arrive from. The old pattern required an empty QUOTED
|
|
572
|
+
// argument and therefore walked straight past the bare call.
|
|
573
|
+
//
|
|
574
|
+
// Measured on the 750-repo corpus: six files contain it and not one is
|
|
575
|
+
// string execution. They are `scripts/verify-extension.js`,
|
|
576
|
+
// `scripts/validate.mjs`, a security ARTICLE about MV3's CSP, and three
|
|
577
|
+
// test files named release-validator, secure-eval and safety. Every one is
|
|
578
|
+
// prose or a policy check that FORBIDS this call - the same shape as the
|
|
579
|
+
// pdf.js CSP probe above, and the same bad look: the tool failing a
|
|
580
|
+
// package for the check that proves the author is eval-aware.
|
|
581
|
+
// Mootong/x-twitter-media-downloader was FAILed on
|
|
582
|
+
// assert(!/\bnew\s+Function\s*\(/.test(source), `new Function() is not allowed: ${filename}`)
|
|
583
|
+
// where the site is the ERROR MESSAGE of a build-time check asserting the
|
|
584
|
+
// shipped files contain no `new Function(` - sole evidence, at FAIL.
|
|
585
|
+
const NEW_FUNCTION_EMPTY_BODY = /^new\s+Function\s*\(\s*(?:(["'`])\s*\1\s*)?\)/;
|
|
586
|
+
// A `(` FOLLOWED IMMEDIATELY BY `?` IS NOT A CALL IN ANY JAVASCRIPT, which
|
|
587
|
+
// makes this the same kind of local proof as the empty argument list: `?`
|
|
588
|
+
// cannot begin an expression, so `eval(?` is a SyntaxError and the only
|
|
589
|
+
// thing that can produce it is a regex group opener - `(?:`, `(?=`, `(?!`,
|
|
590
|
+
// `(?<`. Two unrelated repos in the widened corpus, and both are regexes
|
|
591
|
+
// ABOUT eval rather than uses of it:
|
|
592
|
+
//
|
|
593
|
+
// ha0z1/New-Bing-Anywhere (2116 stars) ships a highlight.js language
|
|
594
|
+
// grammar in `dist/chromium/app/assets/index.js`, a 356 KB line whose
|
|
595
|
+
// keyword alternation contains `|eval(?:cmd)?|`. That was the SOLE
|
|
596
|
+
// evidence for a FAIL on a shipped build.
|
|
597
|
+
// magicelk235/Viaduct-CLI validates a manifest's CSP with
|
|
598
|
+
// `/(?:^|[\s;'"])unsafe-eval(?:[\s;'"]|$)/.test(csp)` - code whose
|
|
599
|
+
// entire purpose is refusing unsafe-eval.
|
|
600
|
+
//
|
|
601
|
+
// Deliberately NOT a rule about regex literals in general. Telling a regex
|
|
602
|
+
// apart from a division needs a parser, and this file has already refused
|
|
603
|
+
// to let a quote-tracking scan silence findings (T-0416); this is one
|
|
604
|
+
// character of local syntax and needs neither.
|
|
605
|
+
const REGEX_GROUP_OPENER = /^(?:eval|new\s+Function)\s*\(\?/;
|
|
567
606
|
const BUNDLER_GLOBAL_SHIM = /^new\s+Function\s*\(\s*(["'])return this\1\s*\)/;
|
|
568
607
|
const EVAL_METHOD_DEF = /^eval\s*\(\s*(?:[A-Za-z_$][\w$]*(?:\s*,\s*[A-Za-z_$][\w$]*)*)?\s*\)\s*\{/;
|
|
569
608
|
const EVAL_NO_ARGS = /^eval\s*\(\s*\)/;
|
|
570
|
-
const NEW_FUNCTION_EMPTY_BODY = /^new\s+Function\s*\(\s*(["'`])\s*\1\s*\)/;
|
|
571
609
|
// EVAL_METHOD_DEF ONLY CATCHES *UNTYPED* METHOD DEFINITIONS (s135, T-0413's
|
|
572
610
|
// second widening). Tardo/OdooTerminal declares
|
|
573
611
|
// `async eval(code: string, options?: Partial<EvalOptions>, isolated_frame?:
|
|
@@ -600,7 +638,7 @@ export const RULES = [
|
|
|
600
638
|
const tail = line.slice(at, at + 80);
|
|
601
639
|
return !BUNDLER_GLOBAL_SHIM.test(tail) && !EVAL_METHOD_DEF.test(tail)
|
|
602
640
|
&& !EVAL_NO_ARGS.test(tail) && !NEW_FUNCTION_EMPTY_BODY.test(tail)
|
|
603
|
-
&& !EVAL_METHOD_DEF_TYPED.test(tail);
|
|
641
|
+
&& !EVAL_METHOD_DEF_TYPED.test(tail) && !REGEX_GROUP_OPENER.test(tail);
|
|
604
642
|
};
|
|
605
643
|
const source = new Map(files.map((f) => [f.path, f.lines]));
|
|
606
644
|
const EVAL_RE = /(^|[^.\w$])eval\s*\(|new\s+Function\s*\(/;
|
|
@@ -1251,14 +1289,68 @@ export const RULES = [
|
|
|
1251
1289
|
"http://www.w3.org/1998/Math/MathML",
|
|
1252
1290
|
"http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
1253
1291
|
"http://www.w3.org/2001/XMLSchema",
|
|
1292
|
+
// s137, T-0413's third widening. The eight above are all w3.org, so the
|
|
1293
|
+
// list encoded "specification namespace" as "a URL at w3.org" and the
|
|
1294
|
+
// wider corpus walked round it immediately: OOXML (every extension that
|
|
1295
|
+
// touches .docx or .xlsx), WS-Trust and XMLDSig (SAML tooling), EPUB,
|
|
1296
|
+
// TTML and Inkscape's SVG extension namespace all arrive at other hosts
|
|
1297
|
+
// and are names by exactly the same argument - fixed by a specification,
|
|
1298
|
+
// never fetched, and unchangeable to https even in principle.
|
|
1299
|
+
"http://schemas.openxmlformats.org/",
|
|
1300
|
+
"http://schemas.microsoft.com/office/",
|
|
1301
|
+
"http://docs.oasis-open.org/ws-sx/",
|
|
1302
|
+
"http://docs.oasis-open.org/wss/",
|
|
1303
|
+
"http://www.w3.org/2000/09/xmldsig#",
|
|
1304
|
+
"http://www.w3.org/ns/ttml",
|
|
1305
|
+
"http://www.idpf.org/2007/opf",
|
|
1306
|
+
"http://www.idpf.org/2007/ops",
|
|
1307
|
+
"http://www.inkscape.org/namespaces/inkscape",
|
|
1308
|
+
"http://sodipodi.sourceforge.net/DTD/",
|
|
1309
|
+
"http://purl.org/dc/elements/",
|
|
1310
|
+
"http://ns.adobe.com/",
|
|
1254
1311
|
];
|
|
1312
|
+
// A LIST OF HOSTS CANNOT KEEP UP, so the general form is POSITION rather
|
|
1313
|
+
// than authority. These three syntactic slots hold a NAME by definition:
|
|
1314
|
+
//
|
|
1315
|
+
// xmlns="..." / xmlns:prefix="..." an XML namespace declaration
|
|
1316
|
+
// createElementNS("...", ...) the namespace argument of the DOM
|
|
1317
|
+
// getElementsByTagNameNS("...", ...) *NS family - always argument one
|
|
1318
|
+
// <!DOCTYPE x PUBLIC "..." "..."> a DTD system identifier
|
|
1319
|
+
//
|
|
1320
|
+
// None of them is a channel anything travels over, none can be rewritten
|
|
1321
|
+
// to https without breaking the document, and no amount of user data can
|
|
1322
|
+
// flow to one. This is what the w3.org/.dtd special case below was
|
|
1323
|
+
// reaching for one host at a time.
|
|
1324
|
+
//
|
|
1325
|
+
// ANCHORED TO WHAT PRECEDES THE OPENING QUOTE, so it cannot exonerate a
|
|
1326
|
+
// fetch: `fetch("http://x")` has `fetch(` before the quote and matches
|
|
1327
|
+
// none of these.
|
|
1328
|
+
const NAME_POSITION =
|
|
1329
|
+
/(?:xmlns(?::[A-Za-z_][\w.\-]*)?\s*=\s*|[A-Za-z]*NS\s*\(\s*|PUBLIC\s+(["'])[^"']*\1\s*|SYSTEM\s+)$/;
|
|
1330
|
+
// A URL WHOSE AUTHORITY CONTAINS `*` IS A MATCH PATTERN, NOT AN ENDPOINT.
|
|
1331
|
+
// `matches: ["http://*/*"]`, `urls: ["http://*/*"]`, documentUrlPatterns,
|
|
1332
|
+
// optional_host_permissions - 26 of the 181 evidence entries this rule
|
|
1333
|
+
// produced over the 750-repo corpus. There is no host to connect to, so
|
|
1334
|
+
// no user data can be sent over it, and "make it https" is not a fix but a
|
|
1335
|
+
// change to which pages the extension covers. Broad access IS reported,
|
|
1336
|
+
// by broad-host-permissions, which reads the manifest keys where it means
|
|
1337
|
+
// something.
|
|
1338
|
+
const WILDCARD_AUTHORITY = /^http:\/\/[^/]*\*/;
|
|
1255
1339
|
const hits = grep(
|
|
1256
1340
|
codeView(files),
|
|
1257
1341
|
/["'`]http:\/\/(?!localhost|127\.0\.0\.1|\[::1\])[^"'`\s]+/i,
|
|
1258
1342
|
(f) => isCode(f) || isMarkup(f),
|
|
1259
|
-
|
|
1260
|
-
|
|
1343
|
+
// PER MATCH, so the scan continues along the same line - see grep() in
|
|
1344
|
+
// scan.mjs. As a post-filter this dropped the whole line when its FIRST
|
|
1345
|
+
// http:// was a namespace, so a real endpoint sharing a line with an
|
|
1346
|
+
// `xmlns` attribute went unreported. An `accept` callback resumes at the
|
|
1347
|
+
// next match instead, the same reason the remote-code skips are written
|
|
1348
|
+
// this way (T-0414).
|
|
1349
|
+
(m, line) => {
|
|
1350
|
+
const url = String(m[0] || "").replace(/^["'`]/, "");
|
|
1261
1351
|
if (XML_NAMESPACES.some((ns) => url.startsWith(ns))) return false;
|
|
1352
|
+
if (WILDCARD_AUTHORITY.test(url)) return false;
|
|
1353
|
+
if (NAME_POSITION.test(line.slice(Math.max(0, m.index - 80), m.index))) return false;
|
|
1262
1354
|
// DOCTYPE system identifiers are the same case as namespaces and were
|
|
1263
1355
|
// missed by the first pass: `<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//
|
|
1264
1356
|
// EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">` names a
|
|
@@ -1272,7 +1364,8 @@ export const RULES = [
|
|
|
1272
1364
|
// document is still a plain-http link and stays reported.
|
|
1273
1365
|
if (/^https?:\/\/(www\.)?w3\.org\/[^"'`\s]*\.dtd$/i.test(url)) return false;
|
|
1274
1366
|
return true;
|
|
1275
|
-
}
|
|
1367
|
+
},
|
|
1368
|
+
);
|
|
1276
1369
|
if (!hits.length) return [];
|
|
1277
1370
|
return [finding({
|
|
1278
1371
|
severity: "warn",
|
|
@@ -1476,8 +1569,46 @@ export const RULES = [
|
|
|
1476
1569
|
return { ...f, text, lines: text.split("\n") };
|
|
1477
1570
|
});
|
|
1478
1571
|
const redirect = String.raw`(?:chrome|browser)\.tabs\.update\s*\(`;
|
|
1572
|
+
// AND THE WINDOW MAY NOT CROSS A BLOCK THAT CLOSES (s137, T-0413's third
|
|
1573
|
+
// widening). 200 characters was picked without a measurement, and the
|
|
1574
|
+
// measurement arrived as this rule's FIRST firing in any corpus run we
|
|
1575
|
+
// have: sindresorhus/notifier-for-github (1978 stars), at FAIL, on
|
|
1576
|
+
// `source/lib/tabs-service.js`, where the two halves are
|
|
1577
|
+
//
|
|
1578
|
+
// line 7: 'chrome://newtab/', <- an element of emptyTabUrls
|
|
1579
|
+
// line 16: return browser.tabs.update(tabId, options);
|
|
1580
|
+
//
|
|
1581
|
+
// and everything between them is `] : [];`, a blank line, and a COMPLETE
|
|
1582
|
+
// `createTab` function. The gap is 193 characters, so the finding existed
|
|
1583
|
+
// by a margin of seven. What that extension does with the list is reuse a
|
|
1584
|
+
// blank tab you already have instead of opening another one - the opposite
|
|
1585
|
+
// of replacing what you get when you open a new tab - and the
|
|
1586
|
+
// `tabs.update` it was paired with is a two-line generic wrapper whose
|
|
1587
|
+
// destination is a parameter.
|
|
1588
|
+
//
|
|
1589
|
+
// The header above already states the intent this misses: the window is
|
|
1590
|
+
// there "so a predicate in one function [does not] pair with a navigation
|
|
1591
|
+
// in an unrelated one further down the file". A character count cannot
|
|
1592
|
+
// express that; a closing brace or bracket at the start of a line can. If
|
|
1593
|
+
// a block ENDED between the two sites they are not on one control-flow
|
|
1594
|
+
// path, and a hijack has to be - the test and the redirect it guards live
|
|
1595
|
+
// in the same body.
|
|
1596
|
+
//
|
|
1597
|
+
// IT IS A NO-OP ON MINIFIED CODE, which is the safe direction: a bundle
|
|
1598
|
+
// has no line starts, so nothing between two sites can match and every
|
|
1599
|
+
// shipped-bundle hijack is judged exactly as before. Shrinking the 200
|
|
1600
|
+
// instead would have hit minified and readable code alike while still
|
|
1601
|
+
// being a number nobody could defend.
|
|
1602
|
+
//
|
|
1603
|
+
// THE LIMIT, STATED RATHER THAN DISCOVERED: a hijack that closes a block
|
|
1604
|
+
// between the guard and the redirect - `if (isNtp(u)) { log(); }` and then
|
|
1605
|
+
// an unguarded `tabs.update` - is now a miss. It joins the aliased-API and
|
|
1606
|
+
// non-tabs.update misses the header already names. Weighed against a
|
|
1607
|
+
// wrong FAIL on a well-known extension, on a rule that has produced no
|
|
1608
|
+
// true positive in any corpus run, that is the trade this file keeps making.
|
|
1609
|
+
const gap = String.raw`(?:(?!\n[\t ]*[}\]])[\s\S]){0,200}?`;
|
|
1479
1610
|
const hits = grepAcross(view, new RegExp(
|
|
1480
|
-
String.raw`(?:${NTP})
|
|
1611
|
+
String.raw`(?:${NTP})${gap}${redirect}` + "|" + String.raw`${redirect}${gap}(?:${NTP})`,
|
|
1481
1612
|
"gi"), isCode);
|
|
1482
1613
|
if (!hits.length) return [];
|
|
1483
1614
|
return [finding({
|