webstore-lint 1.0.25 → 1.0.26

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/rules.mjs +66 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webstore-lint",
3
- "version": "1.0.25",
3
+ "version": "1.0.26",
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
@@ -665,7 +665,72 @@ export const RULES = [
665
665
  // separately so a 4 MB content script produces the same FAIL as a 4 KB
666
666
  // one; the size of the file is the tool's problem, not the developer's.
667
667
  .concat(grepLarge(oversized, EVAL_RE, { filter: isCode, code: true, accept: executesAString }));
668
- split(evals, (shipped, tests) => shipped.length
668
+ // A DECLARED SANDBOX PAGE IS WHERE CHROME TELLS YOU TO PUT eval, and until
669
+ // s147 this rule FAILed the developers who followed that advice. Found by
670
+ // running the corpus against the new code-search index: vladlavrik/netify
671
+ // declares `"sandbox": {"pages": ["sandbox.html"]}` with a sandbox CSP of
672
+ // `... 'unsafe-inline' 'unsafe-eval'`, and its one eval site is
673
+ // `services/sandbox/executor/SandboxExecutor.ts` - the executor that RUNS
674
+ // in that page. That is the documented, sanctioned escape hatch, and we
675
+ // were reporting it at the top severity.
676
+ //
677
+ // This is the worst direction for a linter to be wrong in, and it is worse
678
+ // than the usual wrong FAIL: it fires hardest on the developer who read
679
+ // the policy and did the compliant thing, which is precisely the developer
680
+ // whose trust the tool needs.
681
+ //
682
+ // A SANDBOX DOES NOT LAUNDER THE WHOLE PACKAGE, so this is a partition and
683
+ // not an exemption. The manifest itself names the files that can never be
684
+ // sandboxed - the service worker and every declared content script - and
685
+ // eval in one of those is a violation whatever else the package declares.
686
+ // Those keep FAILing. Everything else becomes a warn that says plainly
687
+ // that the tool cannot tell which side of the boundary the call sits on,
688
+ // because from the file alone it genuinely cannot.
689
+ //
690
+ // AN EMPTY `sandbox.pages` IS NOT A SANDBOX, and a custom sandbox CSP that
691
+ // REMOVES 'unsafe-eval' is not one either - Chrome's default sandbox CSP
692
+ // grants it, so absence of a custom CSP means granted, but an explicit one
693
+ // that drops the token means the page cannot eval and the finding stands.
694
+ const sandboxPages = (manifest?.sandbox?.pages || []).filter(Boolean);
695
+ const sandboxCsp = manifest?.content_security_policy?.sandbox;
696
+ const sandboxAllowsEval = sandboxPages.length > 0
697
+ && (typeof sandboxCsp !== "string" || /'unsafe-eval'/.test(sandboxCsp));
698
+
699
+ // Named by the manifest, so this is not a guess about the file's role.
700
+ const norm = (p) => String(p).replace(/^\.?\//, "").toLowerCase();
701
+ const neverSandboxable = new Set();
702
+ if (manifest?.background?.service_worker) neverSandboxable.add(norm(manifest.background.service_worker));
703
+ for (const s of manifest?.background?.scripts || []) neverSandboxable.add(norm(s));
704
+ for (const cs of manifest?.content_scripts || []) {
705
+ for (const j of cs?.js || []) neverSandboxable.add(norm(j));
706
+ }
707
+
708
+ let evalsForSplit = evals;
709
+ if (sandboxAllowsEval) {
710
+ const hard = evals.filter((h) => neverSandboxable.has(norm(h.file)));
711
+ const soft = evals.filter((h) => !neverSandboxable.has(norm(h.file)));
712
+ evalsForSplit = hard;
713
+ // Emitted whenever there are any, never gated on the FAIL being absent -
714
+ // the same reasoning as the website partition above. A package with one
715
+ // service-worker eval and four sandbox-plausible ones must report all
716
+ // five, or the partition has quietly become a deletion.
717
+ if (soft.length) {
718
+ out.push(finding({
719
+ severity: "warn",
720
+ title: `eval() or new Function(), in a package that declares a sandboxed page (${soft.length} site(s))`,
721
+ detail:
722
+ "Your manifest declares sandbox.pages, and Chrome permits eval inside a sandboxed page - it is the "
723
+ + "documented way to execute a string in an extension. None of these sites is in your service worker "
724
+ + "or in a declared content script, so this tool cannot tell from the file alone whether they run "
725
+ + "inside the sandbox. If they do, they are allowed and you can ignore this. If any of them is "
726
+ + "reached from an ordinary extension page instead, it is still the violation Google names."
727
+ + ` Sandboxed page(s) declared: ${sandboxPages.join(", ")}.`,
728
+ evidence: soft,
729
+ }));
730
+ }
731
+ }
732
+
733
+ split(evalsForSplit, (shipped, tests) => shipped.length
669
734
  ? finding({
670
735
  severity: "fail",
671
736
  title: "eval() or new Function() executes a string as code",