Desktop lint: /<script[\s\S]*?<\/script>/gi in a feed sanitiser scored as
'script injection' and failed pinned-source-validate for rss-reader. Mask
JS regex literals for the markup-shaped rule only; <script in a string
literal (an innerHTML payload) and createElement('script') still fail.
Install scanner: "printenv" as a whole-string entry of a read-only
allowlist (frozenset({..., "printenv"})) fired dump_all_env high →
caution on hermes-jev. Extend the literal-token demotion: a token that is
the ENTIRE quoted literal on a line that executes nothing steps down like
an alternation member; "sudo" inside subprocess.run([...]) and
os.system("printenv") keep high.
A/B vs origin/main: attack probes identical (23 rows), in-tree sweep 319
entries 0 worse/0 changed; both new tests red on base. Bumps
PLUGIN_SCANNER_VERSION to v6 so cached caution verdicts refresh.
Signed-off-by: teknium1 <teknium1@users.noreply.github.com>
77 lines
3.5 KiB
Python
77 lines
3.5 KiB
Python
"""Static admission lint for a plugin's Desktop surface (``desktop/plugin.js``).
|
|
|
|
A ``plugin.js`` is evaluated as ESM in the Electron renderer realm with the app's full authority
|
|
(``apps/desktop/src/contrib/runtime-loader.ts`` says so in its header: error isolation only, no
|
|
capability boundary). The loader accepts that for files the user put on disk; a catalog install is a
|
|
remote source, so listed plugins must stay inside the SDK surface. This lint refuses the moves that
|
|
step outside it. It is a tripwire for review, not a sandbox.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
# (rule, regex) applied to comment-stripped source; every hit fails the "desktop surface" check.
|
|
_FORBIDDEN: Tuple[Tuple[str, "re.Pattern[str]"], ...] = (
|
|
("prototype patching",
|
|
re.compile(r"\b[A-Za-z_$][\w$]*\.prototype\.[\w$]+\s*=[^=]")),
|
|
("prototype patching",
|
|
re.compile(r"\bObject\.definePropert(?:y|ies)\(\s*[\w$.]+\.prototype\b")),
|
|
("prototype patching",
|
|
re.compile(r"\b(?:Reflect|Object)\.setPrototypeOf\(|\.__proto__\s*=")),
|
|
("dynamic code evaluation",
|
|
re.compile(r"(?<![\w$.])eval\(|\bnew\s+Function\(")),
|
|
("dynamic import outside the SDK",
|
|
re.compile(r"\bimport\(\s*(?(?:/[\w/-]*)?['\"]\s*\))")),
|
|
("script injection",
|
|
re.compile(r"createElement\(\s*['\"]script['\"]\s*\)|<script\b")),
|
|
)
|
|
|
|
_COMMENT = re.compile(r"/\*.*?\*/|(?<![:\w])//[^\n]*", re.S)
|
|
|
|
# A JS regex literal (``/<script[\s\S]*?<\/script>/gi``) matches markup, it cannot inject any: a
|
|
# feed sanitiser that STRIPS script tags is the opposite of the move the rule refuses. Regex
|
|
# literals are masked for the markup-shaped rules only; a ``<script`` inside a string literal is
|
|
# still the payload of an ``innerHTML`` write and keeps firing. The lookbehind keeps division
|
|
# (``a / b / c``) from reading as a literal.
|
|
_REGEX_LITERAL = re.compile(r"(?<![\w)\]])/(?:[^/\\\n\[]|\\.|\[(?:[^\]\\\n]|\\.)*\])+/[a-z]*")
|
|
_MARKUP_RULES = frozenset({"script injection"})
|
|
|
|
|
|
def _mask_regex_literals(source: str) -> str:
|
|
return _REGEX_LITERAL.sub(lambda m: " " * len(m.group(0)), source)
|
|
|
|
|
|
def desktop_surface_findings(source: str) -> List[Tuple[str, int]]:
|
|
"""Return ``[(rule, line)]`` for every forbidden construct in a plugin.js source."""
|
|
stripped = _COMMENT.sub(lambda m: "\n" * m.group(0).count("\n"), source)
|
|
no_regex = _mask_regex_literals(stripped)
|
|
findings: List[Tuple[str, int]] = []
|
|
for rule, pattern in _FORBIDDEN:
|
|
haystack = no_regex if rule in _MARKUP_RULES else stripped
|
|
for match in pattern.finditer(haystack):
|
|
findings.append((rule, haystack.count("\n", 0, match.start()) + 1))
|
|
return sorted(findings, key=lambda f: f[1])
|
|
|
|
|
|
def check_desktop_surface(report, plugin_dir: Path) -> None:
|
|
"""Fail the report when ``desktop/*.js`` steps outside the SDK surface; silent when there is none."""
|
|
desktop = Path(plugin_dir) / "desktop"
|
|
if not desktop.is_dir():
|
|
return
|
|
hits: List[str] = []
|
|
for js in sorted(desktop.rglob("*.js")):
|
|
try:
|
|
source = js.read_text(encoding="utf-8", errors="replace")
|
|
except OSError:
|
|
continue
|
|
rel = js.relative_to(plugin_dir).as_posix()
|
|
hits.extend(f"{rule} ({rel}:{line})" for rule, line in desktop_surface_findings(source))
|
|
report.add(
|
|
"desktop surface", not hits,
|
|
"; ".join(hits[:8]) + (f" (+{len(hits) - 8} more)" if len(hits) > 8 else "")
|
|
if hits else "stays inside the plugin SDK surface",
|
|
)
|