fix(coderd): reject workspace proxy hostname prefixes (#27544)

A workspace proxy hostname prefix could be accepted as a valid proxy
access URL. An authenticated user could then be redirected to an
attacker-controlled domain with an application-connect API key in the
URL.

Require proxy access URL matches to have a hostname boundary after the
candidate hostname, allowing only the end of the URL, a port, or a
path.

Add regression coverage for proxy access URL and wildcard hostname
prefixes.

Refs: https://linear.app/codercom/issue/PLAT-384

---------

Co-authored-by: Bobby Ho <bobbidinho@gmail.com>
This commit is contained in:
George K
2026-07-28 13:20:59 -07:00
committed by GitHub
co-authored by Bobby Ho
parent 09a69e624a
commit 8cc7f2bb0e
4 changed files with 63 additions and 2 deletions
+52
View File
@@ -2045,6 +2045,16 @@ func TestProxyByHostname(t *testing.T) {
accessURL: "https://two.coder.com",
wildcardHostname: "*--suffix.two.coder.com",
},
{
name: "three",
accessURL: "https://three.coder.com:8443",
wildcardHostname: "*.wildcard.three.coder.com",
},
{
name: "four",
accessURL: "https://four.coder.com/",
wildcardHostname: "*.wildcard.four.coder.com",
},
}
for _, p := range proxies {
dbgen.WorkspaceProxy(t, db, database.WorkspaceProxy{
@@ -2075,6 +2085,34 @@ func TestProxyByHostname(t *testing.T) {
allowWildcardHost: true,
matchProxyName: "one",
},
{
name: "MatchAccessURLWithPort",
testHostname: "three.coder.com",
allowAccessURL: true,
allowWildcardHost: false,
matchProxyName: "three",
},
{
name: "MatchAccessURLWithTrailingSlash",
testHostname: "four.coder.com",
allowAccessURL: true,
allowWildcardHost: false,
matchProxyName: "four",
},
{
name: "RejectAccessURLPrefix",
testHostname: "one.coder",
allowAccessURL: true,
allowWildcardHost: false,
matchProxyName: "",
},
{
name: "RejectAccessURLTLDPrefix",
testHostname: "one.coder.co",
allowAccessURL: true,
allowWildcardHost: false,
matchProxyName: "",
},
{
name: "MatchWildcard",
testHostname: "something.wildcard.one.coder.com",
@@ -2082,6 +2120,13 @@ func TestProxyByHostname(t *testing.T) {
allowWildcardHost: true,
matchProxyName: "one",
},
{
name: "RejectWildcardHostnamePrefix",
testHostname: "something.wildcard.one.coder",
allowAccessURL: false,
allowWildcardHost: true,
matchProxyName: "",
},
{
name: "MatchSuffix",
testHostname: "something--suffix.two.coder.com",
@@ -2089,6 +2134,13 @@ func TestProxyByHostname(t *testing.T) {
allowWildcardHost: true,
matchProxyName: "two",
},
{
name: "RejectSuffixHostnamePrefix",
testHostname: "something--suffix.two.coder",
allowAccessURL: false,
allowWildcardHost: true,
matchProxyName: "",
},
{
name: "ValidateHostname/1",
testHostname: ".*ne.coder.com",
+1 -1
View File
@@ -24212,7 +24212,7 @@ WHERE
(
(
$2 :: bool = true AND
url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]?%)*'
url SIMILAR TO '[^:]*://' || $1 :: text || '([:/]%)*'
) OR
(
$3 :: bool = true AND
+1 -1
View File
@@ -120,7 +120,7 @@ WHERE
(
(
@allow_access_url :: bool = true AND
url SIMILAR TO '[^:]*://' || @hostname :: text || '([:/]?%)*'
url SIMILAR TO '[^:]*://' || @hostname :: text || '([:/]%)*'
) OR
(
@allow_wildcard_hostname :: bool = true AND