Skip to content

impl: strict URL validation #164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Changed

- URL validation is stricter in the connection screen and URI protocol handler

## 0.6.0 - 2025-07-25

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.v2.models.Workspace
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
Expand Down Expand Up @@ -107,6 +108,11 @@ open class CoderProtocolHandler(
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$URL\" is missing from URI")
return null
}
val validationResult = deploymentURL.validateStrictWebUrl()
if (validationResult is Invalid) {
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "\"$URL\" is invalid: ${validationResult.reason}")
return null
}
return deploymentURL
}

Expand Down
38 changes: 38 additions & 0 deletions src/main/kotlin/com/coder/toolbox/util/URLExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
package com.coder.toolbox.util

import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.coder.toolbox.util.WebUrlValidationResult.Valid
import java.net.IDN
import java.net.URI
import java.net.URL

fun String.toURL(): URL = URI.create(this).toURL()

fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
val uri = URI(this)

when {
uri.isOpaque -> Invalid(
"The URL \"$this\" is invalid because it is not in the standard format. " +
"Please enter a full web address like \"https://example.com\""
)

!uri.isAbsolute -> Invalid(
"The URL \"$this\" is missing a scheme (like https://). " +
"Please enter a full web address like \"https://example.com\""
)
uri.scheme?.lowercase() !in setOf("http", "https") ->
Invalid(
"The URL \"$this\" must start with http:// or https://, not \"${uri.scheme}\""
)
uri.authority.isNullOrBlank() ->
Invalid(
"The URL \"$this\" does not include a valid website name. " +
"Please enter a full web address like \"https://example.com\""
)
else -> Valid
}
} catch (_: Exception) {
Invalid(
"The input \"$this\" is not a valid web address. " +
"Please enter a full web address like \"https://example.com\""
)
}

fun URL.withPath(path: String): URL = URL(
this.protocol,
this.host,
Expand All @@ -30,3 +63,8 @@ fun URI.toQueryParameters(): Map<String, String> = (this.query ?: "")
parts[0] to ""
}
}

sealed class WebUrlValidationResult {
object Valid : WebUrlValidationResult()
data class Invalid(val reason: String) : WebUrlValidationResult()
}
13 changes: 7 additions & 6 deletions src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.settings.SignatureFallbackStrategy
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.coder.toolbox.util.toURL
import com.coder.toolbox.util.validateStrictWebUrl
import com.coder.toolbox.views.state.CoderCliSetupContext
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.jetbrains.toolbox.api.ui.components.CheckboxField
Expand Down Expand Up @@ -69,16 +71,11 @@ class DeploymentUrlStep(

override fun onNext(): Boolean {
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
var url = urlField.textState.value
val url = urlField.textState.value
if (url.isBlank()) {
errorField.textState.update { context.i18n.ptrl("URL is required") }
return false
}
url = if (!url.startsWith("http://") && !url.startsWith("https://")) {
"https://$url"
} else {
url
}
try {
CoderCliSetupContext.url = validateRawUrl(url)
} catch (e: MalformedURLException) {
Expand All @@ -98,6 +95,10 @@ class DeploymentUrlStep(
*/
private fun validateRawUrl(url: String): URL {
try {
val result = url.validateStrictWebUrl()
if (result is Invalid) {
throw MalformedURLException(result.reason)
}
return url.toURL()
} catch (e: Exception) {
throw MalformedURLException(e.message)
Expand Down
92 changes: 92 additions & 0 deletions src/test/kotlin/com/coder/toolbox/util/URLExtensionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,96 @@ internal class URLExtensionsTest {
)
}
}

@Test
fun `valid http URL should return Valid`() {
val result = "http://coder.com".validateStrictWebUrl()
assertEquals(WebUrlValidationResult.Valid, result)
}

@Test
fun `valid https URL with path and query should return Valid`() {
val result = "https://coder.com/bin/coder-linux-amd64?query=1".validateStrictWebUrl()
assertEquals(WebUrlValidationResult.Valid, result)
}

@Test
fun `relative URL should return Invalid with appropriate message`() {
val url = "/bin/coder-linux-amd64"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"/bin/coder-linux-amd64\" is missing a scheme (like https://). Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `opaque URI like mailto should return Invalid`() {
val url = "mailto:user@coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"mailto:user@coder.com\" is invalid because it is not in the standard format. Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `unsupported scheme like ftp should return Invalid`() {
val url = "ftp://coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"ftp://coder.com\" must start with http:// or https://, not \"ftp\""),
result
)
}

@Test
fun `http URL with missing authority should return Invalid`() {
val url = "http:///bin/coder-linux-amd64"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"http:///bin/coder-linux-amd64\" does not include a valid website name. Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `malformed URI should return Invalid with parsing error message`() {
val url = "http://[invalid-uri]"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The input \"http://[invalid-uri]\" is not a valid web address. Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `URI without colon should return Invalid as URI is not absolute`() {
val url = "http//coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"http//coder.com\" is missing a scheme (like https://). Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `URI without double forward slashes should return Invalid because the URI is not hierarchical`() {
val url = "http:coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"http:coder.com\" is invalid because it is not in the standard format. Please enter a full web address like \"https://example.com\""),
result
)
}

@Test
fun `URI without a single forward slash should return Invalid because the URI does not have a hostname`() {
val url = "https:/coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("The URL \"https:/coder.com\" does not include a valid website name. Please enter a full web address like \"https://example.com\""),
result
)
}
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy