Skip to content

feat: security & limits settings, verified updates, start-up fixes, accessible donut, quality fixes - #583

Open
jojofox0815 wants to merge 1 commit into
KnifeLemon:mainfrom
jojofox0815:improvements/hardening-startup-a11y-quality
Open

jojofox0815 wants to merge 1 commit into
KnifeLemon:mainfrom
jojofox0815:improvements/hardening-startup-a11y-quality

Conversation

@jojofox0815

@jojofox0815 jojofox0815 commented Oct 25, 2026 •

Copy link
Copy Markdown

Note

This is one large PR on purpose, so the whole proposal can be reviewed and tried together. I know CONTRIBUTING prefers small PRs. If you'd rather take it in pieces, tell me which parts you want and I'll split them into separate PRs (each part below is self-contained and has its own tests).

What does this change?

A set of improvements from a code review of Filee: a new Settings → Security and limits page (documents with macros, picture limits, time limits, parallel files), verifiable updates, code signing only when a certificate is there, fixes for the two start-up problems, a hardened release workflow, smaller classes, a donut that works with screen readers and the keyboard, and fixes for several conversion-quality issues.

Fixes #450, fixes #416, fixes #506, fixes #433, fixes #468, fixes #406. Related: #486 (README workaround for the CET crash), #377, #384, #524.

1. Documents with macros and other active content (LibreOffice)

  • Before a document goes through LibreOffice, a scanner lists what it carries: VBA macros (module names and auto-run entry points such as AutoOpen, Document_Open, Workbook_Open), ActiveX controls, external relationships, DDE fields, ODF Basic/script libraries and event listeners, linked sections/objects, macro storages in DOC/XLS/PPT including Excel 4.0 macro sheets.
  • Policy in the settings: Ask (default), Convert, Refuse. Ask shows one window per job with every finding: trust and convert / leave these files out / cancel, plus "Don't ask again".
  • Folder watching and the CLI never ask (filee-cli --active-content ask|convert|refuse; --json lists the findings).
  • LibreOffice's worker profiles get macro execution disabled, no link updates, BlockUntrustedRefererLinks and VBA Executable=false (existing entries in registrymodifications.xcu are kept).
  • I checked that headless --convert-to does not run document macros today (LibreOffice 26.8, a dom:load macro in an ODT and a Basic UDF in an ODS). That's why there's no "run macros" switch: the profile settings keep it that way if LibreOffice's behaviour ever changes.

2. Limits and time limits

  • Largest picture in megapixels (default 500), read from the header before decoding, so a few-KB "image bomb" is refused with a clear message (error.image_too_large). Also: longest side, ImageMagick memory and disk.
  • Parallel files (instead of Environment.ProcessorCount).
  • Time limit per step for built-in engines (default 15 min, 0 = none; error.conversion_timeout). Engines that run another program keep their own limits (IHasOwnTimeout).
  • ProcessRunner no longer waits forever for a killed process's output; the msiexec log is deleted after a successful engine install.

3. Updates

  • FileeExpectedPublisher (empty by default): an update installer must be signed by exactly this publisher, even if the installed copy is unsigned.
  • FileeUpdatePublicKey (ECDSA P-256, empty by default): the release's SHA256SUMS must carry a valid .sig, on every platform, or the update is refused. Keys and signatures come from build/sign-checksums.ps1; release.yml and sign-release.ps1 sign when the key is there.
  • Without the two properties nothing changes. SECURITY.md and docs/RELEASE-SECURITY.md describe both.

4. Code signing only when possible

sign-windows.ps1 / sign-release.ps1 find a valid code-signing certificate themselves, and warn and go on unsigned when there is none. -RequireSignature turns that into an error.

5. Start-up problems

  • CET crash on old Windows 10 (the README workaround from docs: README readability pass and a Windows startup fix for old Windows 10 builds with CET #486): Filee.Setup is built with CETCompat=false, so Setup always starts. On Windows 10 (build < 22000), Setup runs filee-cli.exe version once after installing. If that dies with 0xC0000409, a machine-wide install applies the README's Set-ProcessMitigation exception, for Filee.exe and filee-cli.exe only. It's noted in the uninstall entry and removed on uninstall. Windows 11 is not probed. The app keeps CET everywhere else.
  • [Bug]: 无法登陆时启动 #450: the General page shows when Task Manager disabled Filee's startup entry (StartupApproved\Run) and offers to enable it again.

6. Release chain

  • All actions are pinned to commit SHAs, with the version in a comment. Least-privilege permissions and timeout-minutes are set per job.
  • New workflows and files: CodeQL, dependency review, Dependabot (actions and NuGet), weekly engine tests, build provenance attestations and an SBOM for release files.
  • docs/RELEASE-SECURITY.md explains each step and suggests next ones: signing in CI instead of on a PC, reproducible builds, signing the Explorer-menu MSIX, and macOS notarization.

7. Splitting large classes (moves only)

Class Split into
JobQueue .Fetch, .Combine, .Dates, .Outputs, .Sweep, .Allocators
UserDataStore SettingsMigrations
UpdateService .Verification, .Linux, .Workspaces
DonutMenu .Input, .Animation, .Rendering
PresetEditorViewModel .Apply, .Visibility

8. Accessible donut (please see #524, happy to align with #537 / #538)

  • Screen readers: the donut is a list. Each slice is a list item with:

    • its label and caption,
    • the reason it's disabled, as help text,
    • selection and invoke.
  • Keyboard: the donut has a focus ring. In the toolbar editor:

    • arrows choose a slice,
    • Enter / Space / F2 / the context-menu key edit it,
    • Ctrl + arrow moves it.

    Screen readers read these keys out as the donut's help text.

  • High contrast: the ring follows the system's contrast theme.

9. Conversion quality

Issue Change
#416, #506 Word → PDF headers A first-page header of its own is drawn on page 1; a tall header gets room instead of overlapping the body.
#433 PDF → Word background A picture covering most of a page with a text layer goes behind the text instead of being dropped (OCR layers unchanged).
#377 merged PDF blank pages After merging, pages are drawn small. A page that's blank in the result but not in its source triggers a second merge with PdfPig. If that fails too, PDFsharp's file is kept.
#468 PSD transparency Composite without alpha but layers with alpha: the layers are flattened.
#406 Markdown → PDF Code blocks in a monospace box that stays together; quotes with a bar and background; list items no longer cut off.
#384 HWP stamps rhwp already draws the floating pictures I could build; no change without a sample (asked in the issue).

10. Tests on non-Korean systems, and App tests in 11 minutes instead of 96

  • App tests: a full run took 96 minutes on a 16-core PC, long enough to look hung.
    • Cause: every language switch wrote the ~1,200 texts one by one, and each write made every DynamicResource in every open window look its key up again. That's 1 to 4 s per switch, and the tests switch languages more than 1,000 times.
    • Fix: LocalizationService now swaps in a new dictionary at once and does nothing when the language stays the same.
    • Result: 11 minutes. A language switch in the app also does one update instead of a thousand.
  • Failures on a German system: 12 tests failed there on main.
    • OfdTests built their XML with the system's number format.
    • The LibreOffice ODS test compared Calc's locale-dependent display text. It also used a 1% width tolerance for Korean text, which LibreOffice measures with its locale's East Asian font.
    • Two preset-editor tests needed a pinned regional format.
  • Engines page: a LibreOffice installed in Program Files showed no version. LibreOffice 26 dropped MsiProductVersion from version.ini. CopyVersions now reads soffice.exe's product version, which also brings back "update recommended" for such copies.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How was it tested?

  • dotnet format --verify-no-changes

  • dotnet build: no new warnings (the same 13 as on main)

  • dotnet test on Windows 11 (de-DE), rebased on main after 2.1.0 (e830916):

    Project Passed Failed Skipped
    Core 618 0 2
    CLI 27 0 1
    Setup 189 0 0
    Platform 81 0 24
    Engines 1821 0 50
    App 1356 0 2

    On main, 12 of these tests failed on this German system. Section 10 above fixes them:

    • OFD: the test wrote Size="4,2333".
    • LibreOffice: Calc's locale-dependent display text and the column widths of Korean text.
    • Two preset-editor tests: they need a regional format pinned.
    • Engines page: it had no version for a LibreOffice installed in Program Files.

    App test time: 96 → 11 minutes. Without this PR, a full App run took 96 minutes on a 16-core PC and looked hung. Most of that went into language switches; see section 10 above.

  • Tried it with filee-cli on Windows 11 (de-DE) with LibreOffice 26.8:

    • The Markdown, DOCX-header and PDF-background fixes were converted and checked visually.
    • An ODT with a dom:load Basic macro:
      • --active-content refuse refuses it, and --json lists Standard.Module1 and dom:load;
      • convert converts it with the notice "nothing in it was run".
    • An ODS with a Basic module is refused on LibreOffice routes (→ xls, → doc). ODS → PDF uses Filee's own reader, which runs nothing, so that route isn't checked.

New tests:

  • ActiveContentScannerTests, HardeningAndQualityTests (Engines);
  • JobQueueSecurityTests, SecuritySettingsTests (Core);
  • UpdateSigningTests, DonutAccessibilityTests, plus render tests for the new page and window (App);
  • CetCheckTests (Setup).

Not covered by tests:

Screenshots (UI changes)

Settings → Security and limits (new page):

page-security-en

Before a document with macros goes through LibreOffice (policy "Ask"):

active-content-en

The donut with the system's high contrast:

donut-high-contrast-en

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files (machine-translated; native speakers please check)
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (OpenMcdf is now also used directly)
  • The PR does one thing (see the note at the top)

🤖 Generated with Claude Code

@github-actions github-actions Bot added the feature New features label Oct 25, 2026
…ccessible donut, quality fixes

One change set in eleven parts. Each part has its own tests; nothing changes for existing settings files (the new
sections read as their defaults).

Security and limits (new settings page "Security and limits", nav.security)
- Documents with active content: before a document goes through LibreOffice, ActiveContentScanner looks for VBA
  macros (module names from vbaProject.bin, auto-run entry points such as AutoOpen / Document_Open / Workbook_Open),
  ActiveX controls, external relationships (attached templates, OLE links), DDE fields, ODF Basic / script libraries
  and event listeners, linked sections and objects, and legacy DOC / XLS / PPT macro storages including Excel 4.0
  macro sheets (OpenMcdf, already shipped through MsgReader, now referenced directly). Policy: Ask (default),
  Convert, Refuse. "Ask" shows ActiveContentWindow once per job with every finding: trust and convert, leave these
  files out, or cancel; "Don't ask again" stores the choice. Folder watching and the CLI never ask
  (filee-cli --active-content ask|convert|refuse; --json lists the findings).
- LibreOffice hardening: every worker profile gets MacroSecurityLevel=3, DisableMacrosExecution, no link updates in
  Writer and Calc, BlockUntrustedRefererLinks and VBA Executable=false in registrymodifications.xcu (other entries are
  kept). Headless --convert-to doesn't run document macros today (checked with LibreOffice 26.8 on a dom:load macro
  and a Basic UDF); this keeps it so if that ever changes.
- Picture limits: largest picture in megapixels (default 500, checked from the header before decoding, so a small
  "image bomb" is refused with error.image_too_large), longest side, ImageMagick memory and disk; 0 = automatic.
- Parallel files (replaces Environment.ProcessorCount in the queue) and a time limit per step for built-in engines
  (default 15 min, 0 = none; error.conversion_timeout). Engines that run another program keep their own limits
  (IHasOwnTimeout). ProcessRunner no longer waits forever for the output of a killed process; the msiexec log is
  deleted after a successful engine install.

Updates
- FileeExpectedPublisher (Directory.Build.props, empty by default): when set, an update installer must be signed by
  exactly this Authenticode publisher, even if the installed copy is unsigned.
- FileeUpdatePublicKey (ECDSA P-256, empty by default): when set, the release's SHA256SUMS file must carry a valid
  signature (<file>.sig) on every platform, or the update is refused (fail closed). build/sign-checksums.ps1 creates
  keys and signatures; release.yml and sign-release.ps1 sign when the key is available. Without the properties
  nothing changes.

Code signing only when possible
- sign-windows.ps1 / sign-release.ps1 look for a valid code-signing certificate themselves
  (build/signing-certificate.ps1), warn and continue unsigned when there is none; -RequireSignature makes a missing
  certificate an error.

Start-up problems
- CET crash (the README's workaround from KnifeLemon#486): Filee.Setup is built with CETCompat=false so Setup always starts. On Windows 10 (< 22000) Setup runs
  "filee-cli.exe version" once after installing; when it dies with 0xC0000409 (shadow stack fail-fast) a machine-wide
  install applies the README's Set-ProcessMitigation exception for Filee.exe and filee-cli.exe only and notes it in
  the uninstall entry; the uninstaller removes it again. Windows 11 isn't probed; the app keeps CET everywhere else.
- KnifeLemon#450: the General page shows when Windows' Task Manager disabled the startup entry (StartupApproved\Run) and offers
  to enable it again.

Release chain (docs/RELEASE-SECURITY.md explains each step and proposes the next ones)
- All actions pinned to commit SHAs (version in a comment), least-privilege permissions and timeouts per job.
- New: CodeQL (C#), dependency review, Dependabot (actions, NuGet), weekly engine tests (LibreOffice and others),
  build provenance attestations and an SBOM for release files.

Splitting large classes (moves only, no behaviour change)
- JobQueue: .Fetch, .Combine, .Dates, .Outputs, .Sweep, .Allocators; UserDataStore: SettingsMigrations;
  UpdateService: .Verification, .Linux, .Workspaces; DonutMenu: .Input, .Animation, .Rendering;
  PresetEditorViewModel: .Apply, .Visibility.

Accessible donut (see KnifeLemon#524)
- DonutMenuAutomationPeer: the donut is a list, each slice a list item with its label (and caption), the reason a
  slice is disabled as help text, selection and invoke patterns; hover and keyboard changes are announced.
- Focusable with a focus ring; in the toolbar editor arrows choose a slice, Enter / Space / F2 / context-menu key edit
  it, Ctrl + arrow moves it (screen readers read the keys out as the donut's help text). High contrast: the ring
  follows the system's contrast theme.

Quality issues
- KnifeLemon#416 / KnifeLemon#506 (Word → PDF): a first-page header of its own is drawn on the first page, and a tall header gets room
  instead of overlapping the body.
- KnifeLemon#433 (PDF → Word): a picture covering most of a page with a text layer goes behind the text instead of being
  dropped (OCR layers keep the old behaviour).
- KnifeLemon#377 (merged PDF with blank pages): after merging, every page is drawn small; a page blank in the result but not in
  its source triggers a second merge with PdfPig, keeping PDFsharp's file if that fails too.
- KnifeLemon#468 (PSD loses transparency): when the composite image has no alpha but the layers do, the layers are flattened.
- KnifeLemon#406 (Markdown → PDF): code blocks in a monospace box that stays together, quotes with a bar and background, list
  items no longer cut off.
- KnifeLemon#384 (HWP stamps): rhwp already draws floating pictures in the cases tested; no change without a sample file.

Tests that failed outside a Korean or English system (German Windows 11 here)
- OfdTests wrote the OFD XML with the system's number format (Size="4,2333" on a German system): invariant now.
- SpreadsheetFormatTests: an ODS by LibreOffice keeps the text Calc showed in its locale ("1.350.000원"), compared
  without separators now; LibreOffice measures Korean text with its locale's default East Asian font, so the XLS
  width gets the same tolerance as the ODS one (the column ratio stays checked).
- The two preset editor tests set the regional format their expected labels use (labels follow it since 2.1.0).
- Engines page: a LibreOffice Filee didn't download now shows its version. LibreOffice 26 dropped MsiProductVersion
  from version.ini, so CopyVersions reads soffice.exe's product version too; this also brings back "update
  recommended" for such copies.

App tests: 96 minutes → 11 minutes (a run looked hung)
- Every language switch wrote the ~1,200 texts into the application's resources one by one, and every write made each
  DynamicResource in every open window look its key up again: 1 to 4 seconds per switch, and the tests switch
  languages over a thousand times. LocalizationService now builds the new texts in a dictionary of their own and puts
  it in the old one's place at once, and does nothing when the language stays. A language switch in the app does one
  update instead of a thousand as well.

Translations: new strings in all 16 languages, pt-PT included (machine-translated; native speakers please check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@KnifeLemon KnifeLemon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks @jojofox0815, this is a lot of careful work, and several parts fix real problems: the 96 → 11 minute App test run, the tests that fail on a German system, and the missing LibreOffice 26 version. I'd like to take those. I can't review or merge this as one PR, though. It touches the update and release chain, Setup, signing, the settings UI and five large classes all at once. In Filee each PR does one thing, and new features start with a plan agreed in an issue. You offered to split it, so yes please. Here's how I'd like it.

Welcome now, one small PR each (no issue needed):

  1. LocalizationService: replace the dictionary at once, and skip the work when the language stays the same, plus its test.
  2. Locale-independent tests: OfdTests invariant formatting, the SpreadsheetFormatTests comparison, the pinned culture in the two preset-editor tests.
  3. Engine small fixes: CopyVersions reading soffice.exe's product version, the ProcessRunner drain timeout, deleting the msiexec log after success.
  4. Workflow hardening, existing workflows only: SHA pinning, least-privilege permissions, timeout-minutes. Please include only the github-actions part of Dependabot so the pins don't go stale. The Windows CI job takes about 63 minutes today, so please give ci.yml some headroom over 90, or land this after (1).
  5. LibreOfficeSecurity (profile lockdown) on its own. CI skips the LibreOffice tests, so please say how you ran them, including HWP through H2Orestart.

Welcome in principle, but please open or continue an issue with a short plan first, then one PR each:
6. #416/#506, #433 and #406: one PR per issue, with before/after renders.

  • #377: let's wait for the reporter's files before adding a render check to every merge.
  • #468: we asked whether the alpha is a separate channel, so let's wait for that answer. Also, "composite has no alpha and any layer has alpha" matches almost every normal PSD (an opaque Background plus a text layer); it would replace Photoshop's composite with a re-compose that drops adjustment layers, layer styles and masks. The new <summary> also landed on top of FlattenXcf's.
  1. #450: the reporter says Filee isn't listed in Startup apps at all, so let's confirm there that StartupApproved is their problem.
  2. Picture and time limits. Defaults matter here: 500 MP and 15 minutes (also in the CLI) would refuse or stop conversions that work today. I'd start with 0 = off. An abandoned step also keeps running after its slot is freed.
  3. Active content. Your own test shows headless LibreOffice runs no macros, so the issue should first agree on what we protect against. Under the default "Ask", /embeddings/ (every DOCX/PPTX chart has one) and attachedTemplate relationships would prompt on very ordinary documents.
  4. Donut accessibility: please coordinate in #524 with @cre8ive-marketing, who has an agreed plan there. Keyboard reordering was deliberately left out for now. (Small correction: this branch's base predates #537/#538; it merges cleanly anyway.)
  5. CodeQL, dependency review, NuGet Dependabot, weekly engine tests, SBOM and attestations: happy to discuss in one issue, as each adds CI time or PR noise.

Not something we'll take, at least for now:

  • Verified updates (FileeUpdatePublicKey, FileeExpectedPublisher, sign-checksums.ps1, the signing step in release.yml). Anything in the update verification chain needs an agreed threat model and key plan first. As written:

    • the key would live both in a GitHub secret and on the signing PC, and as job-level env it is visible to every step of the release job, including build, tests and a third-party action;
    • sign-release.ps1 doesn't require a signature when the build has a key, so one missed env var would lock those copies out of updates;
    • there is no key rotation, and a lost key means a permanent lock-out;
    • the signature doesn't bind the version (FindFiles picks the installer by suffix), so an older signed release could be replayed under a new tag.

    If you'd like to pursue it, please open an issue with the threat model, and we can talk there.

  • "Sign only if possible" in sign-windows.ps1 / sign-release.ps1. A release without a signature should fail: every signed installed copy refuses an unsigned update, and certificate auto-discovery could pick the wrong certificate.

  • The CET probe and Set-ProcessMitigation in Setup. We decided to keep CET on and document the workaround (README/FAQ). Setup shouldn't change a Windows security mitigation without asking; -Name applies machine-wide by file name, -Remove would also delete settings an admin made, and 0xC0000409 isn't specific to CET. If Setup itself not starting is the problem, that could be its own issue.

  • The class splits. They aren't moves only (the JobQueue and UpdateService splits carry the new behaviour), and the churn would conflict with other open PRs. Let's leave the structure as it is for now.

  • The parts of docs/RELEASE-SECURITY.md / SECURITY.md / README that describe the declined pieces. Windows signing stays on the maintainer's PC with SimplySign.

Translations: German (de.json) landed after your base. Don't worry about it; we'll add the German strings as each piece lands.

Thanks again. Pieces 1–3 can go in quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New features

Projects

None yet

2 participants