Where the v9 pass could only say “the renderer implies nodeIntegration:true but we never read preload.js,”
this pass cracks the 100 MB portable binary, extracts the Electron main process, and turns that inference into a
confirmed finding — a malicious .oph file that reaches host code execution.
.oph preset can achieve renderer code execution and,
through an unvalidated file-write IPC, code execution on the host. The sign-in gate is still a self-described “fake security camera.”
Ophis (branded PSYFR, shipped as Ophis v12) is an offline date-projection / cycle-prediction tool — Gann-style cycle work in astronomical dress. You give it two anchor dates (X-Dates); it counts the days between them (Y), runs a table of little formulas that transform Y into offsets, and projects Z-Dates that it scores against resonance number-sets and eclipse tables.
The engine itself is legitimate and interesting. The security posture is not. Because the whole app is un-obfuscated and ships its own source, a full white-box read was possible — and it exposes a clean, verifiable path from “open a shared preset file” to “attacker runs code on your machine.” The rest of this report is the reproducible method, the anatomy, the v9→v12 delta, and the five findings ranked by severity — plus exactly how to build the hardened version.
new Function() over file-controlled formulas → renderer exec; nodeIntegration:true + unvalidated file-write IPC → host exec.
Sign-in is client-side theatre — 5 hard-coded SHA-512 hashes shipped in config; re-enabling it crashes init.
executeJavaScript built by string concat; headless CLI log-forging from file-derived strings.
.exe to readable source, step by stepThe reproducible spine of the paper. Note the discipline: identify the container from bytes before running anything.
MZ header → Windows PE. Machine field 0x14C → 32-bit. String scan finds Nullsoft at offset 81276 → NSIS. package.json → win.target: "portable". No app.asar string in the clear → payload is compressed.
electron-builder’s NSIS package embeds a 7z archive. A standalone extractor reads it directly — no admin install, no detonation.
app.asar, then unpack itPull the app bundle out of the 7z, then use Electron’s own asar tool to expand its tar-like format into a normal file tree.
main.js (Electron main process), preload.js (the electronBridge definition), and the real ophis.html bootstrap — the files that convert every “inferred” caveat into a verified one.
# standalone extractor — no admin, no running the app
npm install 7zip-bin
ZA=node_modules/7zip-bin/win/x64/7za.exe
# the NSIS .exe IS a 7z container — list, then extract the app bundle
"$ZA" l Ophis_v12_Windows.exe # → resources\app.asar (32,524,448 bytes)
"$ZA" e Ophis_v12_Windows.exe "resources/app.asar" -o./extracted
# unpack the asar → full source tree + main.js + preload.js + ophis.html
npx asar extract extracted/app.asar extracted/unpacked_full
The OPHIS.html in this folder is not the bootstrap — it’s the v9 Field Guide (byte-identical to Ophis_v9_Explained.html). The real loader ophis.html lives inside the asar. Every §5 citation refers to that internal file.
| Layer | What | Notes |
|---|---|---|
| Outer | NSIS portable stub (32-bit PE) | Self-extracts to %TEMP% and runs Ophis.exe |
| Runtime | Electron 39.2.4 · Chromium + Node · ia32 | Ophis.exe 210 MB unpacked; ffmpeg.dll, ICU, 56 locale paks |
| App | resources/app.asar (32.5 MB) | Uncompressed archive; "main": "main.js" |
| Renderer | ophis.html + 24 src/*.js + lib/* | Hand-rolled serial <script> loader with a cache-buster |
The bootstrap injects first-party modules in strict order, appending ?v=<rand> every launch
(ophis.html:549–550) — dev-style loading shipped to production. The CSP (ophis.html:72)
sets script-src 'self' 'unsafe-inline' 'unsafe-eval' — it explicitly permits the new Function()
engine, so it provides no mitigation. Un-obfuscated throughout: real names, comments, TODOs, even debugger; left in.
| Area | v9 | v12 |
|---|---|---|
| Headless / CLI | — | New. --headless, --output-path, … runs windowless, writes files (main.js:490–529) |
| Console→CLI bridge | — | New ophis_logging.js pipes console.* → logToCli |
| T-Dates | — | New second input-date type |
| File validation | single strict path | 3 modes; GUI defaults to LOOSE (most permissive) |
| Default ops | 15 | 16 — adds X2+YxOPH_HEP, enabled by default |
| Astronomy lib | commented out | Loaded & live (ophis.html:39) |
| Sign-in gate | disabled, sha512 commented out | Unchanged — still both |
Net: v12 = v9 + headless CLI + T-Dates + a 16th default op + a validation-mode system whose GUI default (LOOSE) widens the attack surface. Core security posture otherwise unchanged — and marginally worse because hostile files are now more likely to be processed.
Every step is cited to extracted source. This chain was inferred in v9; with main.js and
preload.js in hand it is now confirmed.
.oph preset.oph extension is registered to the app; open-file / second-instance handlers auto-load it (main.js:304–346, 532–548).new Function("Y", …)ophis_model__validation.js:158new Function compiles the un-stripped body. Validator ≠ executor.electronBridge.autoSaveToFile(path, data)preload.js:4; forwarded over IPC to the main process.fs.mkdirSync(recursive) + fs.writeFile — no path checkmain.js:267–283This is an offline, single-user tool — the realistic attacker is a malicious preset file shared in the community (“seed this .oph”), double-clicked by a victim. Under that model, the two Critical findings chain to full host compromise from opening a shared file, which is exactly how these presets circulate.
new Function() over .oph-controlled stringsThe engine compiles every operation string loaded from a file to a native function. Reachable on file open / recalc
with no UI interaction and no sign-in. The math.js “validation” is bypassable because it inspects a
stripped skeleton (prefix removed, oph_* names deleted, Y→10) while new Function
compiles the un-stripped body — validator and executor never see the same code. Import does no content validation of the string, and the v12 GUI default LOOSE auto-repairs hostile files rather than rejecting them.
nodeIntegration:true confirmed + unvalidated file-write IPCv9 could only infer this. main.js:367 sets nodeIntegration:true. contextIsolation is
left unset (defaults true in Electron 39), so the page talks to Node through the contextBridge
electronBridge API — the real Node boundary. And one bridge method, autoSaveToFile(path, contents),
lands in a main-process handler that does fs.mkdirSync(recursive) + fs.writeFile with
no path validation. Any renderer code can write arbitrary content to an arbitrary path with the app’s privileges —
escalating Finding #1 to persistent host execution. nodeIntegration:true here is also incoherent with
isolation-on: it grants the page nothing yet becomes instant full-Node exposure if anyone ever sets contextIsolation:false.
Disabled by flag (author’s comment: “a false sense of security… like a fake security camera”). When on, it does a
client-side compare of sha512(input) against 5 hard-coded, unsalted, single-round SHA-512 digests
shipped in ophis_config.js — offline-crackable and bypassable by editing the flag. Latent crash: sha512.min.js
is commented out of the bootstrap, so re-enabling the flag alone throws ReferenceError in init.
executeJavaScript string-building with a 2-char escaperThe main process pushes file data into the renderer by concatenating JS and calling executeJavaScript,
with an escaper that only handles \ and ". The obvious break is closed on modern V8 (JSON payload,
quotes escaped), but it’s fragile hand-rolled escaping on a privileged sink — any future change reopens injection.
Prefer structured argument passing or an ipcRenderer data channel.
File-derived strings (event names, paths, error text) reach the CLI stream unsanitised via
console.* → logToCli → console.log, so a crafted .oph can inject fake
OPH_ERROR: lines, newlines, or terminal escape sequences into whatever consumes headless output.
Log-integrity / terminal-injection, not code execution.
Paste-ready recommendations. These are proposed, not yet applied to your files — say the word and I’ll implement and verify each.
new Function with a ~60-line shunting-yard evaluator that accepts only numbers, Y, the 4 constants, the oph_* functions, and + − * / ( ) — making validator and executor the same code.nodeIntegration:false, contextIsolation:true (explicit), sandbox:true, keep the preload.autoSaveToFile writes to an allowed directory; reject absolute paths and .. traversal; prefer routing saves through dialog.showSaveDialog like saveFileAs already does.'unsafe-eval' (possible once the parser lands) and move inline handlers out to drop 'unsafe-inline'.esc() helper on every innerHTML interpolation of the anchor label / notes (PSYFR1.html:961, 982).PSYFR1.html ≡ Natori-On-PSYFR-Main-UI.html; keep one as source of truth, generate the other.The rewrites (PSYFR / Natori) are already a cohesive, dependency-free, offline single-file UI — genuinely portfolio-grade. Fixing their own new Function + label-innerHTML smells makes the story land: “I found these bug classes in the target, then made sure not to ship them myself.” Commit the un-obfuscated src/, this report, a METHOD.md from §2, and a SECURITY.md from §6 — and keep the 100 MB .exe out of git.