github fabriziosalmi/certmate v2.48.4
v2.48.4 (scripts run only with the page's nonce, and three more places a key restricted to domains is held to them)

6 hours ago

v2.48.4 (scripts run only with the page's nonce, and three more places a key restricted to domains is held to them)

A patch release with two subjects. The Content-Security-Policy no longer allows eval or inline script: script-src is 'self' and a nonce made for each response, which closes #314. And a key restricted with allowed_domains is refused the instance's configuration, is given an inventory record only when its scope covers every name of the certificate, and gets cache statistics for its own domains only. Two fixes in the dashboard: the certificate dialog stays open when it is reopened while it is still closing, and the loading indicator stays while a request is out. Four dependencies of the MCP server and of the CSS build are updated for advisories, none of them in the image. The API contract moves to 2.44: no field changes shape, and five routes change their answer for a restricted key (see "The API contract" below).


Read this before upgrading

If you use API keys with allowed_domains

Some things belong to the instance and are filed under no domain. A key that carries allowed_domains now gets 403 DOMAIN_OUT_OF_SCOPE on five more routes, as it already does on the backup and client-certificate routes:

  • GET /api/settings and GET /api/web/settings;
  • GET /api/settings/dns-providers and GET /api/web/certificates/dns-providers;
  • GET /api/storage/info.

The settings used to answer such a key with a view narrowed to its domains. That view is removed: one refusal in place of a list of fields to narrow. What a restricted key may know about its own domains it reads from the certificate routes. The SDK's dns_providers() and the MCP server's settings tools answer the refusal to such a key.

Two answers also change in content:

  • The inventory (GET /api/inventory, the detail and the report) gives such a key a record only when its scope covers every name of the certificate, which is the rule of the certificate routes. One name in scope used to be enough.
  • The cache statistics (GET /api/cache/stats and the dashboard's GET /api/web/cache/stats) list the entries for certificates its scope covers, and total_entries counts those.

Sessions and keys without allowed_domains are not affected. Each refusal is in the audit log.

If something in front of CertMate adds script to its pages, or you keep a changed copy of the templates

The pages now send:

Content-Security-Policy: ... script-src 'self' 'nonce-<new for every response>'; ...

'unsafe-eval' and 'unsafe-inline' are gone from script-src. A browser runs a script on a CertMate page only if it is a file served by CertMate or a <script> block carrying that response's nonce. It does not run an event-handler attribute (onclick="..."), a javascript: address, or a string given to eval.

CertMate's own pages need none of those any more. Two cases outside it do:

  • A proxy that injects a snippet into the pages (an analytics tag, a bot check). It has to give its <script> the nonce it reads from the header, or serve the code as a file from the same origin. A proxy that replaces the header with its own still decides the policy, as before.
  • A changed copy of the templates. An inline <script> needs nonce="{{ csp_nonce }}". An onclick no longer runs: a control names an action instead (data-click="name", with data-args='[...]' for its arguments) and the page registers the function under that name with CertMate.actions({...}).

style-src keeps 'unsafe-inline'; styles are not part of this change.

The API contract

The contract version moves from 2.43 to 2.44. Nothing is added or removed and no field changes shape. The five routes above change status code for a key that carries allowed_domains, and the inventory and the cache statistics change what they list for such a key. As in 2.43, these are new answers to existing requests, counted as the security fixes they are.


Security

  • The script policy allows neither eval nor inline script (#314, in three parts).
    • No eval (#1186). The pages run the @alpinejs/csp build of Alpine, at the same version as before (3.15.12), which parses the expressions in the markup instead of compiling them. Of the 530 expressions in the templates, 22 said something that build does not evaluate and became methods or getters of their components.
    • No markup carries script (#1189). The event-handler attributes are gone from the templates and from the markup the scripts build. A control names what it does (data-click, data-change, data-input), its arguments are JSON, and one listener looks the name up among the actions the page registered.
    • A nonce for each response (#1192). The <script> blocks the templates write carry it, and so does the one in the Swagger page.
  • A key restricted to domains is refused the instance's configuration (#1184), above.
  • The inventory holds a restricted key to every name of a certificate (#1184), above.
  • The cache statistics follow the key's scope (#1181), above.

Fixed

  • The certificate dialog stays open when it is reopened while it closes (#1190). The dialog fades for 200 ms before it is hidden, and closing it gives the focus back to its row. Escape followed at once by Enter opened it again while the fade was running, and the timer of the first close then hid it, emptied it and left the focus on nothing. Opening the dialog now calls a pending close off. Reachable from the keyboard only: with a pointer, the backdrop covers the rows until the dialog is hidden.
  • The loading indicator stays while work is out (#1198). When a request ends, the indicator fills its bar and is hidden half a second later. A renewal refused at once leaves the focus on its button, and Enter then started another before the half second was over: the timer of the first hid the indicator of the second, and the page looked idle with a renewal out. Showing the indicator now calls a pending hide off.

Changed

  • The MCP server's dependencies (mcp/): proxy-addr 2.0.8 (#1188, GHSA-jqcg-44mw-7w3h) and @modelcontextprotocol/sdk 1.31.0 (#1197, GHSA-6qxp-vccf-f47h). The first advisory is about a proxy trust setting and the second about the SDK's OAuth client. The server sets no proxy trust and speaks over stdio, so neither condition is met in it; the versions move anyway. The MCP server is not part of the image.
  • The CSS build's dependencies: source-map-js 1.2.2 (#1187, GHSA-68fv-2mgg-jv7q) and postcss-selector-parser 7.1.6 (#1194, GHSA-rj75-hqrm-r3gf). Both are used only when static/css/tailwind.min.css is built and neither is in the image. The second is forced with an npm overrides entry, because Tailwind 3 asks for the 6 series and that series has no fixed release; the bundle built with it is byte for byte the committed one.

Tests

  • The walk with restricted keys says where it cannot see (#1181). The walk compares what a restricted key is answered with the names the instance holds; where the owner's own answer names nothing, a clean result says nothing. Those routes are now written down, each with its reason or with the test that looks at it on an instance that holds names, and a new route of that kind fails the test until someone has decided.
  • Every form of a download is asked by every kind of caller (#1182): 210 combinations of file, format and key options on the two download routes, each by seven callers, every answer opened and searched for a private key.
  • The guard against the real certbot stays in until the process ends (#1183). It was removed when the last test ended, and a job a test had queued could still run after that.
  • The browser suite runs for its own tests and for the dashboard's routes (#1185). A pull request that changed only a tests/test_ui_*.py file, or only modules/web/, used to be merged without the suite having run.
  • Every Alpine expression is checked against the build's own parser in CI (#1186), and the vendored file is compared with the package's.
  • The log scrubber is held to growing linearly, not to a number of seconds (#1191). The test timed hostile input against a CPU budget and failed on a slower runner with the scan unchanged. It now compares the cost of a text with that of one sixteen times as long, at two sizes.
  • An application built by a test stops with the module that built it (#1195). create_app() leaves stopping its scheduler and threads to the end of the process, and the suite builds 284 applications: 274 schedulers were still running at the last test, each firing a job every minute in whatever test ran then. One test that measures the memory of the whole process failed on that in CI. The suite's fixtures now stop each application when its module ends.
  • The browser suite dismisses the first-run wizard once, on the instance (#1196). The fixture that was meant to do it sent a request that was refused and did not read the answer, so fourteen modules each hid the wizard for themselves and one raced it. A test that had been skipped in every run for a reason that was not true (test_welcome_banner_visible) runs again.

How it was checked

  • The script policy, in a browser. Chrome 154 headless, real clicks and key presses, an instance of the previous code beside one of this release, the same data on both. 65 recorded scenarios (every shape a control had, every expression that changed) give the same result on both. A sweep of every page, every Settings tab, the create panel, /docs/ and /redoc reports the same messages on both and no new policy violation.
  • What the policy stops. A test puts markup into three pages itself, in the five forms inline script takes (a handler attribute, an onerror, a javascript: address, a <script> element, a string given to eval). None runs, and the browser names the directive that refused each. The same <script> element runs once it carries the page's nonce.
  • The nonce. New for every response, the same in the header and in the page for /login, /redoc and /docs/, and on every <script> a template writes. Six deliberate breakages of the policy, each caught.
  • The controls. 88 actions are named by controls and 88 are registered, checked in both directions; a name registered by a file that does not have the function fails at load.
  • Restricted keys. On an instance that holds names: the five configuration routes answer the owner 200 and a restricted viewer, operator and stored admin key 403; the inventory lists the key's own certificate and not the one that also covers a name outside its scope; the cache statistics name the key's entry only. Each rule removed in turn fails its test.
  • The dialog. With the page's timers held, so that the time between the two keys is not left to the machine: Escape then Enter, and two closes then Enter, fail on the previous code and pass on this one; the same keys with the fade finished in between pass on both.
  • The loading indicator. A renewal refused at once, Enter on the same button, the second renewal left without an answer: the indicator is gone on the previous code and shown on this one, in real time and with the page's timers held. With the half second allowed to pass in between it is shown on both.
  • The dependencies. Each new version's integrity value is the registry's. The MCP server's own tests pass with the new lock. The CSS bundle rebuilt from scratch is identical to the committed one, and the same build with a parser that throws fails, so the build does go through the package that changed. The advisory's 400 KB selector takes 6.1 s on the old parser and 0.2 s on the new.
  • The release gate runs on the release commit: the suite, the browser tests, and real certificates from Let's Encrypt staging.

Not verified

  • Other browsers. Every browser measurement in this release is Chromium.
  • ReDoc's worker. /redoc creates a worker from a blob: address, which the policy refuses, as it did before this release. The page renders; what the worker would have added was not examined.
  • The loading indicator and the keyboard. The indicator covers the page for the pointer and not for the keyboard: Enter on a focused button reaches it while the indicator is up. That is how the second renewal above starts, and it is not changed in this release.

Don't miss a new certmate release

NewReleases is sending notifications on new releases.