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.
- package/package.json +1 -1
- 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.
|
|
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
|
-
|
|
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",
|