Skip to content

Harden cleanup handling and packaged renderer loading#5

Merged
chrisjcthomas merged 3 commits into
mainfrom
codex/security-scan-fixes
Mar 23, 2026
Merged

Harden cleanup handling and packaged renderer loading#5
chrisjcthomas merged 3 commits into
mainfrom
codex/security-scan-fixes

Conversation

@chrisjcthomas

@chrisjcthomas chrisjcthomas commented Mar 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Problem:
    • The security scan identified a real cleanup authorization bug: file cleanup trusted a prefix-based root check and accepted renderer-supplied cleanup targets after only schema validation.
    • Packaged builds also allowed a remote renderer override through ELECTRON_RENDERER_URL, and the renderer shell had no CSP.
  • Goal:
    • Close the confirmed cleanup tampering path, harden packaged renderer loading, and keep the packaged smoke and visual flows green.
  • Acceptance criteria:
    • Cleanup file removal rejects sibling-prefix escapes and only applies findings that still match the latest main-process scan results.
    • Packaged builds ignore ELECTRON_RENDERER_URL and load the local renderer shell.
    • The renderer shell has an in-repo CSP without regressing packaged startup.
    • Required validation commands pass.

Validation

  • Commands run:
    • npm run validate
    • npm run smoke:packaged
    • npm run test:visual
  • Artifact tested:
    • npm run dev
    • release/win-unpacked/WSA Manager.exe
    • release/WSA.Manager.Setup.<version>.exe

Codex review

  • @codex review requested on this PR, or automatic reviews are enabled
  • Extra review focus was added when needed

UI evidence

  • Visual reference used:
    • Existing packaged visual baselines in tests/e2e/__screenshots__/visual.packaged.spec.ts/
  • Target window sizes:
    • 1400x920, 1060x740, 920x640
  • Baselines changed:
    • No
    • Yes
  • Screenshots:
    • N/A

Notes

  • Risks:
    • Electron sandbox: true was evaluated but breaks the packaged preload bridge in the current app architecture, so this PR keeps sandbox: false and focuses on the confirmed exploit path plus packaged renderer hardening.
    • The CSP allows unsafe-eval because the stricter variant broke packaged runtime; it still blocks remote scripts and tightens object/base/form usage.
  • Follow-ups:
    • Make the preload bundle sandbox-compatible, then re-attempt Electron renderer sandboxing.

Open with Devin

Summary by CodeRabbit

  • Bug Fixes

    • Cleanup now validates items against the latest scan before removal, and path comparisons are more robust across platforms to avoid accidental deletions.
  • Security

    • Added a restrictive Content Security Policy to the renderer to reduce exposure to unsafe resources.
  • Startup

    • App now uses the packaged renderer on releases and development URL only when running in development.
  • Tests

    • Added and strengthened unit and visual tests, including stricter empty-state assertions and updated screenshot tolerance.

@chrisjcthomas

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai

coderabbitai Bot commented Mar 23, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@chrisjcthomas has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 17 minutes and 57 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b208827-53b9-4668-a89c-55e59f13b86f

📥 Commits

Reviewing files that changed from the base of the PR and between f314a19 and 5e31000.

📒 Files selected for processing (2)
  • tests/e2e/visual.packaged.spec.ts
  • tests/main/cleanup-service.test.ts
📝 Walkthrough

Walkthrough

Renderer loading is now conditional on packaging state; cleanup operations are validated against the latest scan snapshot before removal; path containment checks were tightened with normalization and Windows case-insensitivity; a CSP meta tag was added; tests updated/added for these behaviors and visual tolerance.

Changes

Cohort / File(s) Summary
Window Loading Logic
src/main/index.ts
Compute rendererUrl only when !app.isPackaged; call loadURL only if truthy, otherwise fallback to loadFile with packaged renderer.
Cleanup Service & Helpers
src/main/services/cleanup/CleanupService.ts, src/main/services/cleanup/helpers.ts
Track latest scan findings in-memory; scan() records snapshots; apply() resolves requested findings against the latest scan via resolveAuthorizedFinding and fails when mismatched; added rememberScannedFindings, resolveAuthorizedFinding, and isMatchingCleanupFinding; changed isPathWithinRoot to use path.relative and Windows-case normalization.
Security Policy
src/renderer/index.html
Added a Content-Security-Policy meta tag with restrictive defaults, allowing localhost connections and permitting unsafe-eval for renderer scripts and inline styles per existing needs.
Tests (unit & e2e)
tests/main/cleanup-helpers.test.ts, tests/main/cleanup-service.test.ts, tests/e2e/visual.packaged.spec.ts
Extended helpers tests for path containment and exact finding matching; added CleanupService.apply tests that mock paths, set latestScanFindings, validate failure/success cases and side effects; adjusted visual tests to assert empty-state UI elements and increased screenshot tolerance for empty-state images.

Sequence Diagram(s)

sequenceDiagram
  participant Test as Client/Test
  participant Service as CleanupService
  participant ScanStore as LatestScanFindings
  participant FS as FileSystem
  participant Settings as SettingsStore

  Test->>Service: call apply(requestedFindings)
  Service->>ScanStore: resolveAuthorizedFinding(requestedFinding.id)
  ScanStore-->>Service: scannedFinding (or null)
  alt scannedFinding matches requestedFinding
    Service->>FS: removeArtifact(authorizedFinding)
    FS-->>Service: removal success
    Service->>Settings: recordCleanup({ packageName, artifactCount })
    Settings-->>Service: ack
    Service-->>Test: return removed=[authorizedFinding], failed=[]
  else no match / missing
    Service-->>Test: return removed=[], failed=[{ id, reason: "latest scan results" }]
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped through code with careful paws,

matched every finding, checked the laws,
paths normalized across each land,
CSP stitched with steady hand,
tests now guard what once was loose—hooray, cleanup on the roost! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two primary changes: hardening cleanup handling with authorization checks and fixing packaged renderer loading to ignore ELECTRON_RENDERER_URL.
Description check ✅ Passed The description comprehensively covers the security problem, goals, and acceptance criteria; includes validation results and detailed notes on risks and follow-ups; mostly aligns with the template despite some unchecked artifact testing items.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/security-scan-fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the security posture of the application by addressing two critical areas: file cleanup authorization and packaged Electron renderer loading. It resolves a security vulnerability in the cleanup service that could allow tampering with file removal targets and hardens the renderer process in packaged builds by enforcing local content loading and implementing a Content Security Policy. These changes aim to prevent potential exploits and improve the overall integrity of the application.

Highlights

  • Cleanup Authorization Hardened: Implemented stricter validation for cleanup operations, ensuring that file removal requests are cross-referenced against the latest scan results to prevent unauthorized path manipulation and sibling-prefix escapes.
  • Packaged Renderer Loading Secured: Modified packaged builds to ignore the ELECTRON_RENDERER_URL environment variable, forcing them to load the local renderer shell and preventing remote code execution vulnerabilities.
  • Content Security Policy Added: Introduced a Content Security Policy (CSP) in the renderer process to mitigate cross-site scripting (XSS) and other content injection attacks by restricting resource loading.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

gemini-code-assist[bot]

This comment was marked as resolved.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/e2e/visual.packaged.spec.ts (1)

73-76: Consider extracting the repeated empty-state assertions into a helper.

Lines 73-76, Line 97-100, and Line 120-123 duplicate the same checks; a helper would keep these assertions consistent across viewport tests.

♻️ Optional refactor
+async function expectSleepingEmptyState(page: Page) {
+  await expect(page.getByText('WSA is currently sleeping')).toBeVisible()
+  await expect(page.getByRole('button', { name: 'Wake WSA' })).toBeVisible()
+  await expect(page.getByRole('button', { name: 'Open Wizard' })).toBeVisible()
+  await expect(page.getByTestId('queue-empty-state')).toBeVisible()
+}

   await app.page.getByTestId('setup-close-button').click()
-  await expect(app.page.getByText('WSA is currently sleeping')).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Wake WSA' })).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Open Wizard' })).toBeVisible()
-  await expect(app.page.getByTestId('queue-empty-state')).toBeVisible()
+  await expectSleepingEmptyState(app.page)

   await closeSetupWizardIfVisible(app.page)
-  await expect(app.page.getByText('WSA is currently sleeping')).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Wake WSA' })).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Open Wizard' })).toBeVisible()
-  await expect(app.page.getByTestId('queue-empty-state')).toBeVisible()
+  await expectSleepingEmptyState(app.page)

   await closeSetupWizardIfVisible(app.page)
-  await expect(app.page.getByText('WSA is currently sleeping')).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Wake WSA' })).toBeVisible()
-  await expect(app.page.getByRole('button', { name: 'Open Wizard' })).toBeVisible()
-  await expect(app.page.getByTestId('queue-empty-state')).toBeVisible()
+  await expectSleepingEmptyState(app.page)

Also applies to: 97-100, 120-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/visual.packaged.spec.ts` around lines 73 - 76, Extract the four
repeated empty-state assertions into a reusable helper (e.g.,
expectEmptyStateVisible or assertEmptyStateVisible) and replace the duplicate
blocks in tests/e2e/visual.packaged.spec.ts (the occurrences around the three
viewport sections) with a single call to that helper; the helper should accept
the Playwright Page or app object and run the four checks: getByText('WSA is
currently sleeping'), getByRole('button', { name: 'Wake WSA' }),
getByRole('button', { name: 'Open Wizard' }), and
getByTestId('queue-empty-state') so all assertions remain consistent.
tests/main/cleanup-service.test.ts (1)

84-107: Consider adding edge case tests.

The helper functions are well-designed. Consider adding tests for:

  • apply() called without prior scan() (empty latestScanFindings)
  • Finding with matching target but different id

These would strengthen confidence in the validation logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/main/cleanup-service.test.ts` around lines 84 - 107, Add two edge-case
tests: one that constructs the service via createService and calls
CleanupService.apply() without calling scan() first (so latestScanFindings is
empty) and asserts the expected no-op or error behavior; and another that uses
createDesktopFinding to create two findings with the same target but different
id values and verifies the validation/matching logic (i.e., that matching is
done by id not target) when calling scan()/apply() on the service. Use the
existing helpers (createService, createDesktopFinding) and reference
CleanupService.apply and CleanupService.scan in your test names/assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/e2e/visual.packaged.spec.ts`:
- Around line 73-76: Extract the four repeated empty-state assertions into a
reusable helper (e.g., expectEmptyStateVisible or assertEmptyStateVisible) and
replace the duplicate blocks in tests/e2e/visual.packaged.spec.ts (the
occurrences around the three viewport sections) with a single call to that
helper; the helper should accept the Playwright Page or app object and run the
four checks: getByText('WSA is currently sleeping'), getByRole('button', { name:
'Wake WSA' }), getByRole('button', { name: 'Open Wizard' }), and
getByTestId('queue-empty-state') so all assertions remain consistent.

In `@tests/main/cleanup-service.test.ts`:
- Around line 84-107: Add two edge-case tests: one that constructs the service
via createService and calls CleanupService.apply() without calling scan() first
(so latestScanFindings is empty) and asserts the expected no-op or error
behavior; and another that uses createDesktopFinding to create two findings with
the same target but different id values and verifies the validation/matching
logic (i.e., that matching is done by id not target) when calling scan()/apply()
on the service. Use the existing helpers (createService, createDesktopFinding)
and reference CleanupService.apply and CleanupService.scan in your test
names/assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7b6da3c-3a60-47a2-9056-0559e04aae69

📥 Commits

Reviewing files that changed from the base of the PR and between a670f7a and ecc7749.

📒 Files selected for processing (7)
  • src/main/index.ts
  • src/main/services/cleanup/CleanupService.ts
  • src/main/services/cleanup/helpers.ts
  • src/renderer/index.html
  • tests/e2e/visual.packaged.spec.ts
  • tests/main/cleanup-helpers.test.ts
  • tests/main/cleanup-service.test.ts

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@chrisjcthomas chrisjcthomas force-pushed the codex/security-scan-fixes branch from ecc7749 to f314a19 Compare March 23, 2026 05:29
@chrisjcthomas chrisjcthomas merged commit e10ccac into main Mar 23, 2026
5 checks passed
@chrisjcthomas chrisjcthomas deleted the codex/security-scan-fixes branch March 23, 2026 05:39

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +289 to +291
private rememberScannedFindings(findings: CleanupFinding[]): void {
this.latestScanFindings = new Map(findings.map((finding) => [finding.id, finding]))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Scoped scan in uninstallApp replaces entire authorization cache, silently invalidating unrelated cleanup findings

rememberScannedFindings at src/main/services/cleanup/CleanupService.ts:289-291 replaces the entire latestScanFindings map on every scan() call. When uninstallApp triggers a package-scoped scan(packageName) (src/main/ipc/registerIpc.ts:48), only that single package's findings are generated (due to the filter at CleanupService.ts:62-63), and then they replace the full map. This silently destroys the authorization snapshot for all other packages' findings from a prior full scan.

User-facing impact: A user performs a full cleanup scan, selects findings across multiple packages, then uninstalls an app from the Installed Apps page. The uninstallApp handler's implicit scoped scan wipes the cache. When the user returns to apply their selected cleanup findings, every finding for other packages fails with "Cleanup item no longer matches the latest scan results." Similarly, if ADB becomes unavailable during the scoped scan, rememberScannedFindings([]) at line 47 clears the entire cache.

Prompt for agents
In src/main/services/cleanup/CleanupService.ts, the rememberScannedFindings method (line 289-291) replaces the entire latestScanFindings map every time scan() is called. When scan(packageName) is called with a specific package (as happens in the uninstallApp IPC handler at src/main/ipc/registerIpc.ts:48), this destroys all previously cached findings for other packages.

Fix: When scan() is called with a packageName argument (a scoped scan), rememberScannedFindings should merge the new findings into the existing map rather than replacing it. One approach:
1. Add a parameter to rememberScannedFindings indicating whether this is a scoped scan.
2. For scoped scans: first remove all existing entries for the scanned packageName, then add the new findings.
3. For full scans (no packageName): replace the entire map as currently done.
4. Also fix the early-return on ADB failure (line 47): when a scoped scan fails, only clear that package's entries rather than the whole map.

Alternatively, you could have the uninstallApp handler call a different method that doesn't mutate the authorization cache.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant