Skip to content

feat: default backend none [WPB-26206] [WPB-26586] [WPB-26624]#5023

Open
Garzas wants to merge 4 commits into
developfrom
feat/default-backend-none
Open

feat: default backend none [WPB-26206] [WPB-26586] [WPB-26624]#5023
Garzas wants to merge 4 commits into
developfrom
feat/default-backend-none

Conversation

@Garzas

@Garzas Garzas commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

https://wearezeta.atlassian.net/browse/WPB-26206


PR Submission Checklist for internal contributors

  • The PR Title

    • conforms to the style of semantic commits messages¹ supported in Wire's Github Workflow²
    • contains a reference JIRA issue number like SQPIT-764
    • answers the question: If merged, this PR will: ... ³
  • The PR Description

    • is free of optional paragraphs and you have filled the relevant parts to the best of your ability

What's new in this PR?

Adds support for app builds that do not ship with a preconfigured default backend. When no backend is configured, the app shows a setup screen and waits for backend configuration from supported configuration sources.

Changes

  • Add default_backend_enabled config flag.
  • Support empty default backend configuration.
  • Show setup landing page when no backend is configured.
  • Support backend setup through:
    • MDM / managed configuration
    • configuration deeplink
    • manually entered configuration link
    • QR code scanned externally by the system camera app
  • Show success state after backend configuration is applied.
  • Continue to the regular login flow after setup.
  • Add support link fallback for empty support URLs using backend websiteURL/support.
  • Support optional supportEmail from the configuration payload without DB migration.
  • Add feedback_menu_item_enabled flag to hide the generic feedback menu item.
  • Update setup/login copy and DE translations.

Validation

  • Setup flow tested with configuration deeplink.
  • Success page verified before login form.
  • Support URL fallback covered by unit test.
  • Static analysis/lint passed before final copy updates.

@Garzas Garzas self-assigned this Jul 3, 2026
@pull-request-size

Copy link
Copy Markdown

Ups 🫰🟨

This PR is too big. Please try to break it up into smaller PRs.

@Suppress("TooGenericExceptionCaught")
private suspend fun fetchSupportEmail(configUrl: String): String? = withContext(Dispatchers.IO) {
try {
val connection = URL(configUrl).openConnection() as HttpURLConnection

@semgrep-code-wireapp semgrep-code-wireapp Bot Jul 3, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-Side Request Forgery occur when a web server executes a request to a user supplied
destination parameter that is not validated. Such vulnerabilities could allow an attacker to
access internal services or to launch attacks from your web server.

🧹 Fixed in commit c50e613 🧹

@Suppress("TooGenericExceptionCaught")
private suspend fun fetchSupportEmail(configUrl: String): String? = withContext(Dispatchers.IO) {
try {
val connection = URL(configUrl).openConnection() as HttpURLConnection

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified a blocking 🔴 issue in your code:

Unvalidated configUrl parameter allows Server-Side Request Forgery (SSRF) attacks via URL.openConnection(), enabling attackers to target internal services or cloud metadata endpoints.

More details about this

The code calls URL(configUrl).openConnection() to fetch configuration data from configUrl without validating that the URL is safe. An attacker can exploit this by supplying a malicious URL through the configUrl parameter—for example, pointing to internal services like http://localhost:8080/admin or http://192.168.1.1:27017 (a MongoDB instance)—to access resources that should be restricted to the server's internal network.

Here's how the attack would work:

  1. An attacker calls storeFromConfigUrl() with a malicious configUrl like http://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS metadata service).
  2. The fetchSupportEmail() function receives this untrusted URL and creates a URL object from it: URL(configUrl).
  3. The .openConnection() method makes a real HTTP request to that attacker-supplied address from the server's context.
  4. The server connects to the internal AWS metadata service and returns credentials or sensitive environment data, which the attacker then receives in the response and parses as JSON.

Since the application trusts the configUrl parameter without validation, it becomes a bridge for the attacker to reach internal or restricted services that the server can access but external attackers normally cannot.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
val connection = URL(configUrl).openConnection() as HttpURLConnection
private data class PredefinedBackend(
val name: String,
val configUrl: String
)
private val predefinedBackends = listOf(
// Only hardcoded, trusted config endpoints may be requested here.
// Add any additional approved backends to this allowlist.
PredefinedBackend(
name = "Production",
configUrl = "https://example.com/deeplink.json"
)
)
suspend fun storeFromConfigUrl(
globalDataStore: GlobalDataStore,
serverLinks: ServerConfig.Links,
configUrl: String
) {
setCurrentBackend(serverLinks)
globalDataStore.setBackendSupportEmail(
backendApiUrl = serverLinks.api,
supportEmail = fetchSupportEmail(configUrl)
)
}
@Suppress("ReturnCount")
suspend fun resolveEmail(context: Context, staticSupportEmail: String): String? {
val staticEmail = staticSupportEmail.trim()
if (staticEmail.isNotBlank()) return staticEmail
val backendApiUrl = currentBackendApiUrl ?: return null
return GlobalDataStore(context.applicationContext).getBackendSupportEmail(backendApiUrl)
}
fun supportPageIntent(): Intent? =
CustomTabsHelper.resolveUrl("")?.let {
Intent(Intent.ACTION_VIEW, Uri.parse(it))
}
@Suppress("TooGenericExceptionCaught")
private suspend fun fetchSupportEmail(configUrl: String): String? = withContext(Dispatchers.IO) {
try {
val trustedConfigUrl = predefinedBackends
.firstOrNull { it.configUrl == configUrl }
?.configUrl
if (trustedConfigUrl == null) {
appLogger.w("Rejected untrusted backend config URL: $configUrl")
return@withContext null
}
val connection = URL(trustedConfigUrl).openConnection() as HttpURLConnection
connection.connectTimeout = CONNECT_TIMEOUT_MILLIS
connection.readTimeout = READ_TIMEOUT_MILLIS
try {
connection.inputStream.bufferedReader().use { reader ->
json.decodeFromString(SupportConfig.serializer(), reader.readText()).supportEmail
?.trim()
?.takeIf { it.isNotBlank() }
}
} finally {
connection.disconnect()
}
} catch (exception: Exception) {
appLogger.w("Failed to read backend support email from config", exception)
null
}
}
@Serializable
private data class SupportConfig(
@SerialName("supportEmail")
val supportEmail: String? = null
)
}
View step-by-step instructions
  1. Stop passing a raw String into URL(configUrl).openConnection(). Replace configUrl: String with a typed backend value that distinguishes trusted predefined URLs from non-network options, for example BackendConfig.Predefined, BackendConfig.Custom, and BackendConfig.NoBackend.

  2. Move the allowed config URLs into a fixed in-code list of predefined backends instead of accepting an arbitrary URL string. For example, store values like BackendConfig.Predefined("Production", "https://example.com/deeplink.json").

  3. Update the call site so it only calls fetchSupportEmail(...) for a trusted predefined backend URL. For example, branch with when (backend) { is BackendConfig.Predefined -> fetchSupportEmail(backend.configUrl); BackendConfig.Custom, BackendConfig.NoBackend -> null }.

  4. Change storeFromConfigUrl(...) to accept the typed backend object, or accept a validated predefined URL only, so untrusted user input cannot reach the network request. This prevents SSRF by making the request target come from a hardcoded allowlist instead of user-controlled data.

  5. Keep the dropdown or selection UI mapped to backend objects, not (name, url) string pairs, so the selected value cannot be swapped with an arbitrary URL. For example, use items = backendConfigs.map { it.name } and resolve the selected entry with when (val backend = backendConfigs[index]) { ... }.

  6. Alternatively, if users must be allowed to enter a custom backend URL, validate it against a strict allowlist before opening the connection. Parse it with val uri = URI(configUrl) and reject it unless the scheme is https and the host is one of a small set of approved domains such as example.com.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by URLCONNECTION_SSRF_FD-1.

You can view more details about this finding in the Semgrep AppSec Platform.

@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants