From 5d06cff029c1ec8c1d1999b1527e3a88fe18267a Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:37:13 -0600 Subject: [PATCH 1/2] Prefer the discovered REST API root over the xmlrpc-derived URL for self-hosted sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `WordPressOrgRestApi(blog:)` built its base URL from `Blog.url(withPath:)`, which rewrites the raw `xmlrpc` string. An older app version could persist an http `xmlrpc` endpoint for an https site (GHSA-qxpr-7v78-mh5g), so the self-hosted REST client — and the application password it carries as a Basic auth header — could be built on http and sent in plaintext. Prefer `restApiRootURL`, the root observed during REST discovery (https for an https site, and always written alongside the application token), falling back to the xmlrpc-derived `wp-json/` URL only when no discovered root was persisted. --- .../Swift/Blog+SelfHostedRestApi.swift | 28 ++++++++++++ .../Swift/WordPressOrgRestApi+WordPress.swift | 9 ++-- .../BlogSelfHostedRestApiTests.swift | 44 +++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 Modules/Sources/WordPressData/Swift/Blog+SelfHostedRestApi.swift create mode 100644 Modules/Tests/WordPressDataTests/BlogSelfHostedRestApiTests.swift diff --git a/Modules/Sources/WordPressData/Swift/Blog+SelfHostedRestApi.swift b/Modules/Sources/WordPressData/Swift/Blog+SelfHostedRestApi.swift new file mode 100644 index 000000000000..0bff4f73ba39 --- /dev/null +++ b/Modules/Sources/WordPressData/Swift/Blog+SelfHostedRestApi.swift @@ -0,0 +1,28 @@ +import Foundation + +extension Blog { + + /// The WordPress REST API root URL to use for self-hosted network requests. + /// + /// Prefers `restApiRootURL` — the root observed during REST API discovery, which is + /// served over https for an https site — over a URL derived from the raw `xmlrpc` + /// string. An older app version could silently downgrade and persist an http `xmlrpc` + /// endpoint for an https site (GHSA-qxpr-7v78-mh5g), and `url(withPath:)` copies that + /// scheme verbatim, so a REST client built from it would carry the application password + /// (sent as a Basic auth header) over plaintext http. + /// + /// `restApiRootURL` is written together with the application token (see + /// `ApplicationPasswordRepository.assign` and `Blog.createRestApiBlog`), so it is always + /// present when the token is. That makes this a discovery-backed choice rather than a + /// scheme-rewriting guess: an intentionally-http site is left on http (its discovered + /// root is http), and an https site uses the https root that discovery observed. + /// + /// Falls back to the `xmlrpc`-derived `wp-json/` URL only when no discovered root was + /// persisted (legacy XML-RPC sign-ins), leaving behavior unchanged for those sites. + public var selfHostedRestApiRootURL: URL? { + if let restApiRootURL, let url = URL(string: restApiRootURL) { + return url + } + return url(withPath: "wp-json/").flatMap { URL(string: $0) } + } +} diff --git a/Modules/Sources/WordPressData/Swift/WordPressOrgRestApi+WordPress.swift b/Modules/Sources/WordPressData/Swift/WordPressOrgRestApi+WordPress.swift index 8d685e197d71..a08bdb03334e 100644 --- a/Modules/Sources/WordPressData/Swift/WordPressOrgRestApi+WordPress.swift +++ b/Modules/Sources/WordPressData/Swift/WordPressOrgRestApi+WordPress.swift @@ -8,11 +8,10 @@ private func apiBase(blog: Blog) -> URL? { return nil } - guard let urlString = blog.url(withPath: "wp-json/") else { - return nil - } - - return URL(string: urlString) + // Prefer the REST root observed during discovery over one derived from the raw + // `xmlrpc` string, which an older app version could have persisted as http for an + // https site (GHSA-qxpr-7v78-mh5g). See `Blog.selfHostedRestApiRootURL`. + return blog.selfHostedRestApiRootURL } extension WordPressOrgRestApi { diff --git a/Modules/Tests/WordPressDataTests/BlogSelfHostedRestApiTests.swift b/Modules/Tests/WordPressDataTests/BlogSelfHostedRestApiTests.swift new file mode 100644 index 000000000000..afc655ee8ef5 --- /dev/null +++ b/Modules/Tests/WordPressDataTests/BlogSelfHostedRestApiTests.swift @@ -0,0 +1,44 @@ +import CoreData +import Testing +@testable import WordPressData + +@MainActor +struct BlogSelfHostedRestApiTests { + private let contextManager = ContextManager.forTesting() + + private func makeBlog(url: String?, xmlrpc: String?, restApiRootURL: String?) -> Blog { + let blog = BlogBuilder(contextManager.mainContext, dotComID: nil).build() + blog.account = nil + blog.url = url + blog.xmlrpc = xmlrpc + blog.restApiRootURL = restApiRootURL + return blog + } + + @Test func prefersDiscoveredHTTPSRootOverDowngradedXMLRPCEndpoint() { + // An older app version could persist an http xmlrpc endpoint for an https site; the + // https REST root observed during discovery must win so the application password is + // never sent over plaintext http. + let blog = makeBlog( + url: "https://example.com", + xmlrpc: "http://example.com/xmlrpc.php", + restApiRootURL: "https://example.com/wp-json/" + ) + #expect(blog.selfHostedRestApiRootURL?.absoluteString == "https://example.com/wp-json/") + } + + @Test func fallsBackToXMLRPCDerivedRootWhenNoDiscoveredRoot() { + // Legacy XML-RPC sign-ins never persisted a REST root, so behavior is unchanged. + let blog = makeBlog( + url: "https://example.com", + xmlrpc: "https://example.com/xmlrpc.php", + restApiRootURL: nil + ) + #expect(blog.selfHostedRestApiRootURL?.absoluteString == "https://example.com/wp-json/") + } + + @Test func returnsNilWhenNeitherRootIsAvailable() { + let blog = makeBlog(url: "https://example.com", xmlrpc: nil, restApiRootURL: nil) + #expect(blog.selfHostedRestApiRootURL == nil) + } +} From 95549dca513e875f61bd1f0912c42564d65ffb65 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 19 Aug 2026 15:53:10 -0600 Subject: [PATCH 2/2] Add a release note --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 67b9ad21ae4b..fa77a30cfe75 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -4,6 +4,7 @@ 27.2 ----- +* [*] Fix an issue where an https self-hosted site's application password could be sent over insecure http when using its REST API [#25914] * [*] Custom Post Types: Make custom post types with REST API and editor support available in My Site [#25849] * [*] Stop the media picker from removing gallery images when you cancel it in the experimental editor [#25866] * [*] Stats: Fix the screen getting stuck on a loading indicator for self-hosted sites that are not connected to Jetpack [#25858]