Skip to content

Commit 82eee1f

Browse files
authored
impl: strict URL validation (#164)
This commit rejects any URL that is opaque, not hierarchical, not using http or https protocol, or it misses the hostname. The rejection is handled in the connection/auth screen and also in the URI protocol handling logic<img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/489964c8-491c-4766-9891-42c63cfd353e" /> <img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/dec6acae-4a5e-4a2a-8e59-69b74ba52a9e" /> <img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/802558be-60dc-43e3-9512-ff9007aa50af" />
1 parent c5f8e12 commit 82eee1f

File tree

5 files changed

+147
-6
lines changed

5 files changed

+147
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Changed
6+
7+
- URL validation is stricter in the connection screen and URI protocol handler
8+
59
## 0.6.0 - 2025-07-25
610

711
### Changed

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.coder.toolbox.sdk.CoderRestClient
99
import com.coder.toolbox.sdk.v2.models.Workspace
1010
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1111
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
12+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
1213
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1314
import kotlinx.coroutines.Job
1415
import kotlinx.coroutines.TimeoutCancellationException
@@ -107,6 +108,11 @@ open class CoderProtocolHandler(
107108
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$URL\" is missing from URI")
108109
return null
109110
}
111+
val validationResult = deploymentURL.validateStrictWebUrl()
112+
if (validationResult is Invalid) {
113+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "\"$URL\" is invalid: ${validationResult.reason}")
114+
return null
115+
}
110116
return deploymentURL
111117
}
112118

src/main/kotlin/com/coder/toolbox/util/URLExtensions.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
11
package com.coder.toolbox.util
22

3+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
4+
import com.coder.toolbox.util.WebUrlValidationResult.Valid
35
import java.net.IDN
46
import java.net.URI
57
import java.net.URL
68

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

11+
fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
12+
val uri = URI(this)
13+
14+
when {
15+
uri.isOpaque -> Invalid(
16+
"The URL \"$this\" is invalid because it is not in the standard format. " +
17+
"Please enter a full web address like \"https://example.com\""
18+
)
19+
20+
!uri.isAbsolute -> Invalid(
21+
"The URL \"$this\" is missing a scheme (like https://). " +
22+
"Please enter a full web address like \"https://example.com\""
23+
)
24+
uri.scheme?.lowercase() !in setOf("http", "https") ->
25+
Invalid(
26+
"The URL \"$this\" must start with http:// or https://, not \"${uri.scheme}\""
27+
)
28+
uri.authority.isNullOrBlank() ->
29+
Invalid(
30+
"The URL \"$this\" does not include a valid website name. " +
31+
"Please enter a full web address like \"https://example.com\""
32+
)
33+
else -> Valid
34+
}
35+
} catch (_: Exception) {
36+
Invalid(
37+
"The input \"$this\" is not a valid web address. " +
38+
"Please enter a full web address like \"https://example.com\""
39+
)
40+
}
41+
942
fun URL.withPath(path: String): URL = URL(
1043
this.protocol,
1144
this.host,
@@ -30,3 +63,8 @@ fun URI.toQueryParameters(): Map<String, String> = (this.query ?: "")
3063
parts[0] to ""
3164
}
3265
}
66+
67+
sealed class WebUrlValidationResult {
68+
object Valid : WebUrlValidationResult()
69+
data class Invalid(val reason: String) : WebUrlValidationResult()
70+
}

src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package com.coder.toolbox.views
22

33
import com.coder.toolbox.CoderToolboxContext
44
import com.coder.toolbox.settings.SignatureFallbackStrategy
5+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
56
import com.coder.toolbox.util.toURL
7+
import com.coder.toolbox.util.validateStrictWebUrl
68
import com.coder.toolbox.views.state.CoderCliSetupContext
79
import com.coder.toolbox.views.state.CoderCliSetupWizardState
810
import com.jetbrains.toolbox.api.ui.components.CheckboxField
@@ -69,16 +71,11 @@ class DeploymentUrlStep(
6971

7072
override fun onNext(): Boolean {
7173
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
72-
var url = urlField.textState.value
74+
val url = urlField.textState.value
7375
if (url.isBlank()) {
7476
errorField.textState.update { context.i18n.ptrl("URL is required") }
7577
return false
7678
}
77-
url = if (!url.startsWith("http://") && !url.startsWith("https://")) {
78-
"https://$url"
79-
} else {
80-
url
81-
}
8279
try {
8380
CoderCliSetupContext.url = validateRawUrl(url)
8481
} catch (e: MalformedURLException) {
@@ -98,6 +95,10 @@ class DeploymentUrlStep(
9895
*/
9996
private fun validateRawUrl(url: String): URL {
10097
try {
98+
val result = url.validateStrictWebUrl()
99+
if (result is Invalid) {
100+
throw MalformedURLException(result.reason)
101+
}
101102
return url.toURL()
102103
} catch (e: Exception) {
103104
throw MalformedURLException(e.message)

src/test/kotlin/com/coder/toolbox/util/URLExtensionsTest.kt

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,96 @@ internal class URLExtensionsTest {
6060
)
6161
}
6262
}
63+
64+
@Test
65+
fun `valid http URL should return Valid`() {
66+
val result = "http://coder.com".validateStrictWebUrl()
67+
assertEquals(WebUrlValidationResult.Valid, result)
68+
}
69+
70+
@Test
71+
fun `valid https URL with path and query should return Valid`() {
72+
val result = "https://coder.com/bin/coder-linux-amd64?query=1".validateStrictWebUrl()
73+
assertEquals(WebUrlValidationResult.Valid, result)
74+
}
75+
76+
@Test
77+
fun `relative URL should return Invalid with appropriate message`() {
78+
val url = "/bin/coder-linux-amd64"
79+
val result = url.validateStrictWebUrl()
80+
assertEquals(
81+
WebUrlValidationResult.Invalid("The URL \"/bin/coder-linux-amd64\" is missing a scheme (like https://). Please enter a full web address like \"https://example.com\""),
82+
result
83+
)
84+
}
85+
86+
@Test
87+
fun `opaque URI like mailto should return Invalid`() {
88+
val url = "mailto:[email protected]"
89+
val result = url.validateStrictWebUrl()
90+
assertEquals(
91+
WebUrlValidationResult.Invalid("The URL \"mailto:[email protected]\" is invalid because it is not in the standard format. Please enter a full web address like \"https://example.com\""),
92+
result
93+
)
94+
}
95+
96+
@Test
97+
fun `unsupported scheme like ftp should return Invalid`() {
98+
val url = "ftp://coder.com"
99+
val result = url.validateStrictWebUrl()
100+
assertEquals(
101+
WebUrlValidationResult.Invalid("The URL \"ftp://coder.com\" must start with http:// or https://, not \"ftp\""),
102+
result
103+
)
104+
}
105+
106+
@Test
107+
fun `http URL with missing authority should return Invalid`() {
108+
val url = "http:///bin/coder-linux-amd64"
109+
val result = url.validateStrictWebUrl()
110+
assertEquals(
111+
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\""),
112+
result
113+
)
114+
}
115+
116+
@Test
117+
fun `malformed URI should return Invalid with parsing error message`() {
118+
val url = "http://[invalid-uri]"
119+
val result = url.validateStrictWebUrl()
120+
assertEquals(
121+
WebUrlValidationResult.Invalid("The input \"http://[invalid-uri]\" is not a valid web address. Please enter a full web address like \"https://example.com\""),
122+
result
123+
)
124+
}
125+
126+
@Test
127+
fun `URI without colon should return Invalid as URI is not absolute`() {
128+
val url = "http//coder.com"
129+
val result = url.validateStrictWebUrl()
130+
assertEquals(
131+
WebUrlValidationResult.Invalid("The URL \"http//coder.com\" is missing a scheme (like https://). Please enter a full web address like \"https://example.com\""),
132+
result
133+
)
134+
}
135+
136+
@Test
137+
fun `URI without double forward slashes should return Invalid because the URI is not hierarchical`() {
138+
val url = "http:coder.com"
139+
val result = url.validateStrictWebUrl()
140+
assertEquals(
141+
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\""),
142+
result
143+
)
144+
}
145+
146+
@Test
147+
fun `URI without a single forward slash should return Invalid because the URI does not have a hostname`() {
148+
val url = "https:/coder.com"
149+
val result = url.validateStrictWebUrl()
150+
assertEquals(
151+
WebUrlValidationResult.Invalid("The URL \"https:/coder.com\" does not include a valid website name. Please enter a full web address like \"https://example.com\""),
152+
result
153+
)
154+
}
63155
}

0 commit comments

Comments
 (0)