mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-29 04:02:36 +01:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d8bf3968 | ||
|
|
b05a666e50 |
No files matched your search
@@ -0,0 +1,32 @@
|
||||
# Chunked Framey frontend audit route, 26 October 2026
|
||||
|
||||
The local 116-plugin Chromium audit served chunked modules at a route that
|
||||
omitted the store receipt, while the production panel requested
|
||||
`/framey/module/<id>/<receipt>/<file>`. The audit parsed that receipt as part
|
||||
of the file path and returned 404. This made its result for chunked frontends
|
||||
weaker than the separate targeted panel checks.
|
||||
|
||||
The audit now accepts the production URL shape only when the plugin ID,
|
||||
receipt and JavaScript path are valid, the receipt equals the catalog SHA-256,
|
||||
and the cached archive bytes match that SHA-256. Invalid IDs, receipts, paths
|
||||
and changed archive bytes fail before the module is served. The production
|
||||
Framey panel and installer are unchanged.
|
||||
|
||||
## Exact archived browser check
|
||||
|
||||
Pinned official Deck Shelves 2.0.0 (store ID 135, SHA-256
|
||||
`b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`)
|
||||
ran through the repaired audit in an opaque Chromium iframe. Its entry module
|
||||
and all eight JavaScript chunks returned HTTP 200 at receipt-bound URLs. The
|
||||
frontend reported ready and called `get_settings`; the audit denied that
|
||||
backend call and a Steam library read. Plugin network access remained blocked
|
||||
by the audit policy. The previous
|
||||
catalog browser observation had failed to resolve `./chunks/...` from a blob
|
||||
URL. This check verifies the current audit route and frontend initializer,
|
||||
not the official backend or a live shelf.
|
||||
|
||||
The focused route regression covers entry and chunk reads plus wrong receipt,
|
||||
path traversal, malformed ID and changed archive bytes. The catalog's
|
||||
**4 works / 50 partial / 51 broken / 11 N/A** statuses are unchanged. Deck
|
||||
Shelves remains partial; installed settings, home-shelf behavior, worn VR and
|
||||
physical controller input remain unverified.
|
||||
@@ -11,6 +11,7 @@ import io
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import zipfile
|
||||
@@ -41,6 +42,27 @@ def render_panel(record, data):
|
||||
return framey_panel.panel(record['id']).replace(
|
||||
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
|
||||
|
||||
|
||||
def read_module_asset(path, records, cache):
|
||||
"""Serve the same receipt-bound module URL shape as the installed panel."""
|
||||
prefix = '/framey/module/'
|
||||
if not path.startswith(prefix):
|
||||
raise ValueError('Invalid module path')
|
||||
parts = path[len(prefix):].split('/', 2)
|
||||
if len(parts) != 3 or not re.fullmatch(r'[1-9][0-9]*', parts[0]):
|
||||
raise ValueError('Invalid plugin ID')
|
||||
plugin_id, receipt, name = parts
|
||||
if not framey_panel.RECEIPT.fullmatch(receipt) or not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module receipt or path')
|
||||
record = records[int(plugin_id)]
|
||||
if receipt != record['sha256']:
|
||||
raise ValueError('Plugin receipt changed')
|
||||
data = (cache/(receipt+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != receipt:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
return archive.read(record['folder']+'/dist/'+name)
|
||||
|
||||
PAGE = '''<!doctype html><meta charset=utf-8><title>Framey catalog frontend audit</title>
|
||||
<h1>Framey catalog frontend audit</h1>
|
||||
<p>Exact archived frontend code runs in an opaque iframe. All backend calls and network requests are denied.
|
||||
@@ -134,15 +156,7 @@ def main():
|
||||
return
|
||||
elif path.startswith('/framey/module/'):
|
||||
try:
|
||||
plugin_id, name = path.removeprefix('/framey/module/').split('/', 1)
|
||||
if not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module path')
|
||||
record = records[int(plugin_id)]
|
||||
data = (args.cache/(record['sha256']+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != record['sha256']:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
body = archive.read(record['folder']+'/dist/'+name)
|
||||
body = read_module_asset(path, records, args.cache)
|
||||
content_type = 'text/javascript; charset=utf-8'
|
||||
module_asset = True
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
|
||||
@@ -5,6 +5,7 @@ import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
@@ -21,6 +22,7 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w') as archive:
|
||||
archive.writestr('Example/dist/index.js', bundle)
|
||||
archive.writestr('Example/dist/chunks/frontend.js', b'export const ready = true;')
|
||||
data = stream.getvalue()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
record = {'id':42, 'folder':'Example', 'sha256':digest}
|
||||
@@ -30,3 +32,20 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
self.assertIn(b'const report=(kind,message)', page)
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.render_panel({**record, 'sha256':'0'*64}, data)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cache = Path(directory)
|
||||
(cache/(digest+'.zip')).write_bytes(data)
|
||||
records = {42:record}
|
||||
route = f'/framey/module/42/{digest}/'
|
||||
self.assertEqual(audit.read_module_asset(route+'index.js', records, cache), bundle)
|
||||
self.assertEqual(audit.read_module_asset(route+'chunks/frontend.js', records, cache),
|
||||
b'export const ready = true;')
|
||||
for bad in (f'/framey/module/42/{"0"*64}/index.js',
|
||||
route+'chunks/../frontend.js', route+'other.json',
|
||||
f'/framey/module/042/{digest}/index.js'):
|
||||
with self.subTest(bad=bad), self.assertRaises((ValueError, KeyError)):
|
||||
audit.read_module_asset(bad, records, cache)
|
||||
(cache/(digest+'.zip')).write_bytes(data+b'changed')
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.read_module_asset(route+'index.js', records, cache)
|
||||
Reference in new issue
Block a user