Repository navigation
feat(updater)!: send update-feed credentials only to the feed's origin - #10270
Conversation
Credential headers from `requestHeaders` / `addAuthHeader` and the feed URL's query string are sent only to download URLs on the feed's origin, the same rule as for redirects. - builder-util-runtime: new `HttpExecutor.removeCrossOriginSensitiveHeaders`, reusing the cross-origin redirect rule and header set. - electron-updater: new `Provider.feedBaseUrl` (generic/s3/spaces/r2, keygen, bitbucket, github, gitlab; PrivateGitHub and custom providers keep sending the headers) and `AppUpdater.downloadRequestHeaders`, applied to every download against its actual URL: full downloads, both blockmaps (the old one against an app-set `previousBlockmapBaseUrlOverride`), differential range requests, the AppImage differential, and the NSIS web package (full and differential, which now use the same per-download headers). - `newUrlFromBase` adds the feed query only to URLs on the feed's origin; a URL on another origin keeps its own query, so pre-signed URLs work. - docs: notes on the auto-update page, the hardening page and the v27 breaking-changes page. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`HttpExecutor.removeCrossOriginSensitiveHeaders` copies the headers with `deepAssign`, which ignores `__proto__`, `constructor` and `prototype` keys, and then deletes the credential headers, like the cross-origin redirect handling does. The input is not mutated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tring `electron-builder migrate-schema` now also prints an advisory (log.warn, config unchanged, linking to its v27 breaking-changes section) for a `generic` publish `url` with a query string: electron-updater sends the feed query and the credential headers only to downloads on the feed's origin. The publish of the root and of each top-level section of the migrated config is checked. JS/TS configs get the same rule on the syntax tree in migrate-schema-programmatic.ts, with the `propValue`, `objectLiteral` and `isLiteral` helpers; a parity test runs each config through both paths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uides - v27 breaking changes: the migrate-schema section lists the advisory for a `generic` publish `url` with a query string, with a tip in the credentials section. - v26-to-v27: checklist items for the credentials change; Step 1 lists the advisory. - what's new in v27 and the CLI page: a row and the advisory list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 52c8ce3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
newUrlFromBase dropped the feed query on an http -> https upgrade of the feed host while the credential headers were kept. It now uses HttpExecutor.isCrossOrigin, the predicate removeCrossOriginSensitiveHeaders uses. The docs and changeset say that the upgrade keeps the feed query too, and that the feed-query rule applies to every provider whose URLs are resolved against the feed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
…dentials Provider.feedBaseUrl now defaults to undefined (not declared) instead of null, so a custom provider no longer silently sends the credential headers to every download URL. When the download headers include a credential header (the set stripped on cross-origin redirects) and the provider does not declare feedBaseUrl, downloadUpdate() fails with ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED before any download request. A provider declares its feed URL (headers only to that origin) or null (headers to every download URL). The built-in providers declare their feed URL; private GitHub declares null, since its asset URLs come from the GitHub API and need the token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
…dirs in manifestDownloadOriginTest Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
A blockmap was always requested from `${url}.blockmap` with the file URL's query string, so a pre-signed installer URL's signature was reused on the blockmap (403, full download). The update manifest can now name the blockmap's own URL in files[].blockMapUrl. resolveFiles resolves it like url (relative to the feed, an absolute URL keeps its own query) and the download follows the feed-origin credential rule. With it, the old blockmap is not derived from the new file's URL: it comes from the cache, else previousBlockmapBaseUrlOverride, else the update is downloaded in full, which caches the new blockmap. The manifest signature covers blockMapUrl as an extra field on the file record only when present, so other manifests canonicalize as before. The private GitHub and GitLab providers resolve files from release assets and ignore it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
… time Plain-JavaScript apps get no type error for these behaviour changes, so electron-updater says so, following the v27 upgrade guardrails: the first download that loses credential headers (cross-origin) and the first that does not get the feed query each log a warning once per updater, naming the headers, the query parameter names and the origins (never values), with the migration-guide link. ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED links the guide, and a signature failure on a manifest with files[].blockMapUrl says the field is signed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
…ry string migrate-schema's advisory for a generic publish url with a query string only reaches users who run it. The build now prints the same warning once per process when it writes such a feed to app-update.yml: electron-updater 7 adds the feed query, and sends the credential headers, only to downloads on the feed's origin. Only the query parameter names are logged, never the url or values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the two inline findings, a few other candidate issues were checked and ruled out: GitLab/private-GitHub providers silently drop files[].blockMapUrl in resolveFiles (so differential updates there just fall back to full downloads, unlike GitHub's mishandling); httpExecutor.ts's fixed SENSITIVE_REDIRECT_HEADERS set is deliberately extensible via registerSensitiveHeader() for custom auth header names; and the generic-feed-query build warning firing once per process in the programmatic multi-app case shares the same root cause as the PublishManager.ts:83 finding already flagged inline, rather than being a separate bug.
Extended reasoning...
This PR reworks credential/header propagation across origins in electron-updater (redirect header stripping, feed-origin-scoped request headers, blockmap/differential download URLs) plus adds a build-time advisory in the electron-builder migrate-schema/publish path — both touch security-sensitive surface (which requests carry auth headers/tokens). Two confirmed findings are already filed inline (GitHubProvider mishandling an absolute pre-signed blockMapUrl, and the feed-query advisory only firing once per process). I additionally verified GitLabProvider/PrivateGitHubProvider never populate blockMapUrl in resolveFiles at all, and confirmed httpExecutor.ts exposes a registerSensitiveHeader() escape hatch for non-standard credential headers, so neither is an additional bug worth a new inline comment.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
packages/electron-updater/src/providers/GitHubProvider.ts— For GitHubProvider, an absolute files[].blockMapUrl (a pre-signed URL on another host) gets mangled into a broken github.com URL instead of being downloaded as-is, unlike every other non-excluded provider. resolveFiles (Provider.ts:175) runs pathTransformer(fileInfo.blockMapUrl) before resolving against baseUrl. GitHubProvider's transformer (GitHubProvider.ts:237) prepends${basePath}/download/${tag}/to the absolute URL string, sonew URL(...)treats it as a relative path on github.com rather than the pre-signed host. Fix: resolveFiles (or GitHubProvider's transformer) must skip pathTransformer when the field is already an absolute URL, so files[].blockMapUrl keeps its own origin as docs promise.Why this was flagged
A GitHub-published app sets a pre-signed files[].blockMapUrl (e.g. https://cdn.example.net/app.blockmap?X-Amz-Signature=...) in latest*.yml, as the new feature in updateInfo.ts:41-46 and the manifest-blockmap-url changeset explicitly support. GitHubProvider.resolveFiles (GitHubProvider.ts:237) passes that absolute URL through pathTransformer = p => getBaseDownloadPath(tag, p.replace(/ /g,'-')) (GitHubProvider.ts:240-246), which returns
${basePath}/download/${tag}/https://cdn.example.net/app.blockmap?X-Amz-Signature=.... resolveFiles (Provider.ts:175) then does new URL(that, baseUrl) where baseUrl is github.com; since the string starts with '/', it is parsed as a relative path on github.com, not redirected to cdn.example.net. The differential download then requests a nonexistent github.com path instead of the pre-signed blockmap, erroring and falling back to a full download on every update check. Only GitLabProvider and PrivateGitHubProvider are documented as ignoring blockMapUrl; GitHubProvider is not.Verification: GitHubProvider.resolveFiles (GitHubProvider.ts:237) uses the transformer
p => this.getBaseDownloadPath(updateInfo.tag, p.replace(/ /g,"-")); getBaseDownloadPath (GitHubProvider.ts:240-246) unconditionally returns${this.basePath}/download/${tag}/${fileName}even when fileName is an absolute URL.
resolveFiles ran the provider's path transformer on blockMapUrl too, so GitHub's release download path turned an absolute pre-signed blockmap URL into a github.com path. An absolute blockMapUrl is now resolved as-is, with its own host and query; a relative one still goes through the transformer like url. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
… per process A multi-platform build with a different generic publish url per platform only warned for the first one. The warning is now deduplicated by the feed url without its query, so each feed is named once and no query value is kept or logged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GZ3qF2fpaZQMQmfYXLDhV
There was a problem hiding this comment.
No new blocking issues. 1 optional suggestion (a nit or a note on pre-existing code) was found and not posted. Nothing in this review needs a push before merging.
1 further finding on code this push did not change was outside the scope of this re-review and not posted.
Clean textual merge (git merge-tree exit 0). Brings in electron-userland#10264 (v27 Windows update changes), electron-userland#10262 (uninstaller icon/CRC), electron-userland#10270, electron-userland#10255 and the rest of master through 581e6f5; validated with compile, the touched suites and ci:validate. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013jjAPT1CNUEKbQyinFCHG3
Summary
requestHeaders/addAuthHeader(headers such asAuthorization, the same set that is removed on a cross-origin redirect) and the query string of the feed URL are sent only to download URLs on the feed's origin (scheme, host and port). This is the same rule as for redirects: anhttp→httpsupgrade of the same host on the default ports keeps them.previousBlockmapBaseUrlOverridekeeps the credentials on that origin.AppUpdater.downloadRequestHeaders(url, downloadUpdateOptions, originUrl?).Provider.feedBaseUrl(electron-updater, new). The generic (also used for s3, spaces and r2), keygen, bitbucket, github and gitlab providers set it; for GitLab its origin ishttps://<host>, where electron-builder publishes the release assets. It isnullby default: the private GitHub provider and custom providers keep sending the request headers to every download URL. A custom provider opts in by overriding the getter, and one that extends a built-in provider inherits it.newUrlFromBaseadds the feed query only to URLs on the feed's origin. A URL on another origin keeps its own query string, so pre-signed URLs work.HttpExecutor.removeCrossOriginSensitiveHeaders(headers, originUrl, targetUrl)(builder-util-runtime, new). It returns the headers without the credential headers whentargetUrlis on another origin thanoriginUrl, using the cross-origin redirect rule and header set, and the headers themselves otherwise. It copies the headers withdeepAssign, which ignores__proto__,constructorandprototypekeys, and does not mutate its input.genericpublishurlwith a query string. The publish of the root and of each top-level section of the migrated config is checked. JS/TS configs get the same rule on the syntax tree, with thepropValue,objectLiteralandisLiteralhelpers that are already on master. The advisory comes last, after the nsis-web, mac entitlements, per-machine and sign-hook advisories.files[].urlandpackages.<arch>.path, so a pre-signed URL has to be inlatest*.ymlbefore it is signed), a bullet on the hardening page, v27 breaking changes (the at-a-glance row and the section "Update credentials stay on the feed's origin", the migrate-schema list and a tip), v26-to-v27 checklist items, what's new in v27 and the CLI page.Breaking changes
latest*.ymlpoints downloads at another origin (another host, or the same host with another port or scheme) that authenticates with yourrequestHeaders/addAuthHeadercredentials, or with a token in the feed URL's query (e.g.url: https://updates.example.com/?key=…), those downloads are now requested without them and can fail (for example with 401 or 403). Serve the update files from the feed origin (relativefiles[].url, the default) or use pre-signed URLs.Provider.feedBaseUrlto apply the feed-origin rule.electron-builder migrate-schemaprints an advisory for agenericpublishurlwith a query string.Tests
Added
http; same-origin blockmaps and range requests keep the credentials and the feed query; the old blockmap frompreviousBlockmapBaseUrlOverride; the NSIS web-package and AppImage differential downloads; providers withoutfeedBaseUrl.removeCrossOriginSensitiveHeaders(another origin, the same origin, the input is not mutated, another port,https→http, thehttp→httpsupgrade, a__proto__key).newUrlFromBasekeeps the base query on the base origin (another origin, another port,https→http, a URL with its own query, a blockmap path).feedBaseUrlof each provider and thenulldefault.Changed
VerifyUpdateFileAuthenticodeSignaturefunctionality to AppUpdater #10239): the download in theverifyUpdateFiletests gets a provider stub withoutfeedBaseUrlinstead ofnull, becauseexecuteDownloadnow reads the provider's feed origin for the download headers.