Skip to content

Promote 1.7.0-beta.8 to main - #374

Merged
Zaldaryon merged 15 commits into
mainfrom
dev
Sep 5, 2026
Merged

Promote 1.7.0-beta.8 to main#374
Zaldaryon merged 15 commits into
mainfrom
dev

Conversation

@Pixnop

@Pixnop Pixnop commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Hotfix promotion. Merge #373 first so main carries 1.7.0-beta.8, then merge this one as a merge commit (not a squash), then tag main.

What players get

Opening Manage Mods went blank for anyone with a mod whose mod database listing carries a null among its category tags, Vanilla Variants being the popular case, reported within eleven hours of beta.7 on the listing and the Discord (#370). The filter derivations added in beta.7 folded case on every entry during the first render and threw on the null. Every reader now keeps string entries only and treats a malformed list, or a null entry inside a valid list, as nothing, so a shape the server does not honour can no longer take the page down (#371).

Nothing else user-facing. The other change on dev since beta.7 is #367, which pins the main process's renderer defenses in the test suite (sandbox, node integration, CSP script sources, navigation, window-open and permission handlers) so a one-line regression on any of them turns CI red. Tests only.

Held back on purpose for beta.9, with a proper test round: the error boundary that turns a page crash into a message and a log line (#372), the readable compatibility verdict on the mod release list (#369), and the login failure log classification (#368).

Before tagging

Confirm package.json on main reads 1.7.0-beta.8 after this merges, then tag from main.

Pixnop and others added 9 commits September 5, 2026 00:31
Six one-line changes in the main process survived the whole suite: sandbox
and nodeIntegration on the window webPreferences, a remote host in the
renderer script-src, the will-navigate guard, the window open handler and
the permission request handler.

The webPreferences flags and the CSP are now read as values (a flag map
parsed out of the BrowserWindow options, a directive map parsed out of the
meta tag) rather than matched as substrings. The handlers stay source-text
pins, anchored so a renamed local or a prettier pass still passes and a
weakened value does not.
…nses

test(security): pin the main-process renderer defenses
The detail endpoint returns a null among Vanilla Variants' category tags.
The Manage Mods filter derivations folded case on every entry during the
first render, so null.toLowerCase() threw, and with no error boundary the
whole page went blank for anyone with that mod installed (#370).

Every reader in installedFilters.ts now goes through one helper that keeps
string entries only and reads a malformed list as empty, so a shape the
server does not honour can no longer take the page down.
Guarding the outer releases field let { releases: [null] } through, and
release.tags then threw on the entry: the same render crash this helper
exists to rule out. Each entry is now checked as well.
fix(mods): stop Manage Mods going blank on a null tag from the mod database
@Pixnop
Pixnop requested a review from Zaldaryon September 5, 2026 12:17

@Zaldaryon Zaldaryon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes

The new head includes PR #373, and the required checks pass. I found two blocking issues in the security contract and malformed ModDB handling that must be fixed before this promotion reaches main.

1. The permission contract accepts a denial in a dead branch

In tests/security-boundaries.test.ts:406-420, findNodes(handler.body, ...) accepts any matching callback anywhere in the handler body. The current negative fixture at lines 511-517 rejects if (false) callback(true) because of the true argument, not because the callback is unreachable. I confirmed that the helper accepts this weakened fixture:

if (false) callback(false)

Please add that exact regression case and make the assertion require a direct, unconditional denial in the executable handler body. Otherwise a future change can leave permission requests conditionally or never denied while this contract test remains green.

2. Malformed release entries can still crash Manage Mods before filtering

The new installedFilters.ts guards only its own reads. useGetCompleteInstalledMods.ts:71-77 still iterates the raw dmod.releases array and reads release.modversion and release.tags first. parseModDetailResponse checks that releases is an array, but it does not validate its members. A response containing { releases: [null] } therefore throws at release.modversion; a release whose tags is null reaches evaluateModCompatibility and throws there.

Please sanitize release entries at the ModDB parser boundary or guard this hook and every other raw release consumer. Add a Manage Mods or hook regression that feeds the raw malformed JSON and verifies that loading completes without an exception. The current installedFilters unit tests do not cover this earlier path.

The five required checks passed on the new head, but these findings prevent approval of the release promotion.

The beta.7 blank Manage Mods page came from a detail payload the parser
waved through: Vanilla Variants carried a null among its tags, and the
first render died reading it. #371 taught the installed filters to read
around that shape, but useGetCompleteInstalledMods walks the raw
releases array before any filter runs, so a `{ releases: [null] }`
response still threw on release.modversion and a release with
`tags: null` still threw inside evaluateModCompatibility.

readModDetail now cleans what it hands out. A release that is not an
object is dropped, its tags become a list of strings only, and the mod's
own tags get the same treatment. Every reader downstream already assumes
exactly that shape, so fixing it once here is smaller than guarding four
call sites and it covers the ones nobody has written yet.

A release whose modversion is not a string is kept with an empty one
rather than dropped, because newestReleaseFileId reads releases[0] to
build the download URL and removing an entry would quietly change which
file "install newest" picks. Every consumer already treats a falsy
modversion as unusable.

Part of #370.
@Pixnop

Pixnop commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Both findings are right, and the second one is the more important: #371 only guarded the filters' own reads while useGetCompleteInstalledMods still walked the raw releases first, so { releases: [null] } still took the page down.

#375 fixes that at the boundary rather than per consumer. readModDetail now guarantees that releases holds objects only, that every release's tags and the mod-level tags are string lists, and that modversion is a string (coerced to "" rather than dropping the entry, because newestReleaseFileId reads releases[0] to build the download URL and a dropped entry would silently shift which file "install newest" picks). I audited every raw reader of the detail object, eight of them across the hook, the release list, the bulk updater, the install page and both modpack importers; all sit behind the parser, nothing bypasses it. The regression feeds the raw malformed JSON through the real parser and hook and asserts the page loads and still offers the right update.

The permission pin is being tightened in a separate PR on tests/security-boundaries.test.ts: a denial has to be a direct statement of the handler's executable body, with if (false) callback(false) and four other dead or conditional shapes as rejected fixtures, and the same rule applied to the window-open and navigation handlers. I will link it here as soon as it is up. This promotion follows dev, so its head will carry both once they land, and I will re-request then.

@Pixnop

Pixnop commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

The permission pin is up as #376. Reproduced your fixture first: on the current dev, if (false) callback(false) leaves all 26 tests green. The same class was live in two more places, the window-open denial behind if (details.url) and a will-navigate guard after an early return, so the new rule covers all six handlers: a denial has to be a direct statement of the executable body, nothing before it able to skip it. Your fixture and twenty others are in the rejected set. Both #375 and #376 target dev; once they land this head carries them and I will re-request.

@Zaldaryon Zaldaryon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested

This re-review is anchored at the unchanged promotion head 1d5280a3db7efc6af7bb2adb32d7214530c1d446. The two blocking findings from the previous review remain, and the security contract review has two additional bypass cases.

High: malformed ModDB releases still crash Manage Mods

moddb.ts:126-137 validates only that releases is an array and returns its raw members. useGetCompleteInstalledMods.ts:71-77 then dereferences each member before installedFilters.ts runs. A response with releases: [null] throws at release.modversion; a release with tags: null throws inside evaluateModCompatibility.

The changed filter tests construct typed in-memory objects and never exercise the parser or this hook, so they do not cover the crashing path. Sanitize release objects and consumed fields at the parser boundary, or guard every raw consumer, then add a parser-to-hook or Manage Mods regression using malformed JSON.

High: the security contract accepts unreachable or conditional denials

assertPermissionRequestHandlerDenies recursively accepts callback(false) anywhere, including if (false) callback(false). assertWindowOpenHandlerDenies similarly accepts a sole denial nested inside a conditional. assertNavigationHandler requires one matching guard somewhere in the body, so an early return can make that guard unreachable while the contract still passes.

These are false-positive security tests: a future renderer defense regression can pass the suite without denying a permission request, window open, or navigation. Require direct, unconditional executable-body denials and add negative fixtures for if (false) callback(false), conditional window denial, and an early return before each navigation guard.

PRs #375 and #376 propose the missing corrections, but neither change is part of this promotion.

Verification

  • Local npm ci passed with no reported vulnerabilities.
  • npm run typecheck passed.
  • npm run lint:ci passed with 15 existing warnings and no errors.
  • npm run format:check passed.
  • Focused installedFilters.test.ts and security-boundaries.test.ts: 54 passed.
  • npm run build:unpack passed.
  • The five required GitHub contexts passed on the exact head. SonarCloud passed and the manual macOS build was skipped by policy.
  • git diff --check passed for the promotion range.

Because these findings remain, I cannot approve or merge the beta.8 promotion.

Zaldaryon and others added 2 commits September 5, 2026 10:44
Sanitize ModDB release entries at the parser boundary
The permission pin from #367 searched the whole handler subtree with
findNodes, so a denial in a branch the runtime never takes satisfied it.
Zaldaryon is right on #374: `if (false) callback(false)` passed.

The rule is now about placement, not only about the argument. A denial
has to be a direct statement of the executable handler body: the
expression body of a one-expression arrow, or a top-level statement of a
block body that nothing before it can skip. Anything nested in an if, a
ternary, a try, a loop or another function no longer counts.

setPermissionCheckHandler and setWindowOpenHandler get the same rule, and
the navigation guards now have to sit in the reachable prefix of their
body, which closes an early return in front of the guard.

@Zaldaryon Zaldaryon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested

PR #375 is now included in this promotion and resolves the malformed ModDB release finding. The current head still contains the older security contract from before #376, so the promotion is not safe to merge into main.

The following helpers still accept denials that the runtime can skip:

  • tests/security-boundaries.test.ts:406-420 recursively accepts callback(false), including if (false) callback(false).
  • tests/security-boundaries.test.ts:435-444 recursively accepts an action: "deny" return inside a conditional.
  • tests/security-boundaries.test.ts:321-366 accepts a navigation guard after an early return because it searches the handler's full direct statement list without modeling reachability.

PR #376 addresses the original forms, but its current head still has additional false-positive cases in the permission-check and reachability helpers. Land a corrected #376, or apply the equivalent fixes to this promotion, then rerun the promotion checks and request a fresh review.

Verification

  • Current promotion head: 5fa04624a76e4433bf16fe86e1c73d51d657fe4d.
  • Current dev points to the same head.
  • The five required contexts pass, as do the Ubuntu and Windows matrices and SonarCloud. macOS is skipped by repository policy.
  • The focused parser, filter, and security suites pass 87 tests, but the security contract still accepts the bypasses above.
  • No merge is eligible while these findings remain.

Comment thread tests/security-boundaries.test.ts
Comment thread tests/security-boundaries.test.ts
Comment thread tests/security-boundaries.test.ts Outdated
…swers with

The check-handler assertion accepted any unconditional false expression, so
`() => { false }` and `() => { false; return true }` both passed while returning
undefined and true. The reachability helper also stopped only at exits written
directly in the block, so a conditional return or throw before a denial left it
unreachable and still accepted.

A denial now has to be the value the handler answers with: an expression body,
or a reachable direct return. And a statement that can leave the handler on any
path, wherever the return, throw, break or continue sits inside it, ends the
reachable run.
@Pixnop
Pixnop requested a review from Zaldaryon September 5, 2026 14:34
…itional

test: require an unconditional denial in the main-process security handlers
@Pixnop

Pixnop commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Both blockers are in: #375 (release entries sanitised at the parser boundary) landed at 13:44 and #376 (denials must be direct, reachable statements of the executable handler body; bare false, discarded expressions, and denials after any statement that can exit are all rejected) landed at 15:06. This head is the current dev tip (7a90aa3), package.json reads 1.7.0-beta.8, and the required checks are green on it. Ready for your look; I will confirm the version on main before tagging.

@Zaldaryon Zaldaryon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both findings on the promotion are closed now that the head has advanced to 7a90aa3.

Finding 1, the AST security contract. #376 merged into dev as 7a90aa3, so this promotion head carries canLeaveTheBody and reachableStatements. I reran the shipped assert helpers against the weakened shapes from the review: if (false) callback(false), if (granted) return; callback(false), and a window-open denial nested in an if are all rejected now, and the four real handlers in src/main/index.ts still pass. npx vitest run tests/security-boundaries.test.ts is 32 passing on this head.

Finding 2, the raw releases walk. #375 is on dev, so src/domain/mods/moddb.ts sanitises every release at the parser boundary: readRelease coerces tags through cleanStrings and modversion to a string and drops entries that are not objects, so useGetCompleteInstalledMods can no longer meet a malformed entry before the filters run.

The promotion range against main is the version bump to 1.7.0-beta.8, the #375 ModDB shape fixes, the #367 and #376 security-boundary pins, and the #371 Manage Mods regression test. #368, #369 and #372 are not in it, which matches the PR description holding them for beta.9. I scanned the full added diff: no AI attribution, no private paths, no credentials, no files outside src/, tests/ and the two package files.

Full local gate on 7a90aa3 is green: typecheck, lint:ci (0 errors, 15 pre-existing hook warnings), format:check, test:coverage at 93.39% statements and 90.18% branches against the 87/85 floors, build:unpack on Electron 44.1.1. All five required GitHub contexts pass on the head, SonarCloud passes, macOS is skipped by policy.

Approving. Merge as a merge commit per the description. Tagging main is the maintainers' release step and is not part of this review.

@Zaldaryon
Zaldaryon merged commit fd48259 into main Sep 5, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants