Skip to content

feat(updater)!: send update-feed credentials only to the feed's origin - #10270

Merged
mmaietta merged 12 commits into
masterfrom
feat/update-feed-origin-headers
Oct 2, 2026
Merged

mmaietta merged 12 commits into
masterfrom
feat/update-feed-origin-headers

Conversation

@mmaietta

@mmaietta mmaietta commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Feed-origin credentials (electron-updater). The credential headers from requestHeaders / addAuthHeader (headers such as Authorization, 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: an http → https upgrade of the same host on the default ports keeps them.
    • It applies to every download, against its actual URL: full downloads, both blockmaps, differential range requests, the AppImage differential download, and the NSIS web package (full and differential, which now use the same per-download headers).
    • The old blockmap from an app-set previousBlockmapBaseUrlOverride keeps the credentials on that origin.
    • New protected 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 is https://<host>, where electron-builder publishes the release assets. It is null by 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.
  • Feed query (electron-updater). newUrlFromBase adds 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 when targetUrl is on another origin than originUrl, using the cross-origin redirect rule and header set, and the headers themselves otherwise. It copies the headers with deepAssign, which ignores __proto__, constructor and prototype keys, and does not mutate its input.
  • migrate-schema (electron-builder). New advisory (log.warn, config unchanged, linking to its v27 breaking-changes section) for a generic publish url with 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 the propValue, objectLiteral and isLiteral helpers that are already on master. The advisory comes last, after the nsis-web, mac entitlements, per-machine and sign-hook advisories.
  • Docs. A note on the auto-update page (with the v27 section: signed update manifests cover files[].url and packages.<arch>.path, so a pre-signed URL has to be in latest*.yml before 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.
  • Changesets. The repo is in changesets pre mode, so the electron-updater major stays on 7.0.0-alpha.
    • manifest-download-credentials: electron-updater major, builder-util-runtime minor;
    • cross-origin-headers-safe-copy: builder-util-runtime patch;
    • migrate-schema-feed-query-advisory: electron-builder patch.

Breaking changes

  • Update files on another origin that need credentials. If latest*.yml points downloads at another origin (another host, or the same host with another port or scheme) that authenticates with your requestHeaders / addAuthHeader credentials, 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 (relative files[].url, the default) or use pre-signed URLs.
  • Custom providers. A custom provider that does not extend a built-in provider keeps sending the request headers to every download URL; override Provider.feedBaseUrl to apply the feed-origin rule.
  • No action if your update files are served from the feed origin.
  • electron-builder migrate-schema prints an advisory for a generic publish url with a query string.

Tests

Added

  • manifestDownloadOriginTest (new): a download on another origin gets no credential headers or feed query; the web package on another origin gets none while the same-origin installer keeps them; blockmaps and range requests on another host or over http; same-origin blockmaps and range requests keep the credentials and the feed query; the old blockmap from previousBlockmapBaseUrlOverride; the NSIS web-package and AppImage differential downloads; providers without feedBaseUrl.
  • httpExecutorTest: removeCrossOriginSensitiveHeaders (another origin, the same origin, the input is not mutated, another port, https → http, the http → https upgrade, a __proto__ key).
  • urlUtilTest: newUrlFromBase keeps the base query on the base origin (another origin, another port, https → http, a URL with its own query, a blockmap path).
  • providerFactoryTest, providerTest and gitlabProviderTest: feedBaseUrl of each provider and the null default.
  • migrateSchemaTest and migrateProgrammaticSchemaTest: the feed-query advisory, the order of all five advisories (nsis-web, mac entitlements, per-machine, sign hook, feed query) and feed rows in the parity table.

Changed

mmaietta and others added 4 commits October 1, 2026 11:39
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-bot

changeset-bot Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 52c8ce3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
builder-util-runtime Minor
electron-updater Major
electron-builder Patch
app-builder-lib Patch
builder-util Patch
dmg-builder Patch
electron-publish Patch
electron-builder-squirrel-windows Patch
electron-forge-maker-appimage Patch
electron-forge-maker-nsis-web Patch
electron-forge-maker-nsis Patch
electron-forge-maker-snap Patch

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

claude added 6 commits October 1, 2026 22:39
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
@mmaietta
mmaietta marked this pull request as ready for review October 2, 2026 13:38

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, so new 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.

Comment thread packages/app-builder-lib/src/publish/PublishManager.ts
claude added 2 commits October 2, 2026 15:23
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

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@mmaietta
mmaietta merged commit ec9135d into master Oct 2, 2026
63 of 64 checks passed
@mmaietta
mmaietta deleted the feat/update-feed-origin-headers branch October 2, 2026 17:40
imlucas pushed a commit to imlucas/electron-builder that referenced this pull request Oct 6, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants