CIMD and registration accepted any URI scheme for client_uri and logo_uri, so allowlisted metadata could surface javascript: links on sign-in. Validate absolute http/https at ingest and only expose those schemes in branding. Signed-off-by: Bryan Frimin <bryan@probo.com>
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package coredata_test
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/uri"
|
|
)
|
|
|
|
func TestOAuth2Client_IsRedirectURIAllowed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
registered []uri.URI
|
|
requested string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact https match",
|
|
registered: []uri.URI{"https://chatgpt.com/connector/oauth/callback"},
|
|
requested: "https://chatgpt.com/connector/oauth/callback",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "https mismatch",
|
|
registered: []uri.URI{"https://chatgpt.com/connector/oauth/callback"},
|
|
requested: "https://evil.example/callback",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "loopback ignores port when registered without port",
|
|
registered: []uri.URI{"http://localhost/callback"},
|
|
requested: "http://localhost:3118/callback",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "loopback ignores port when registered with a different port",
|
|
registered: []uri.URI{"http://127.0.0.1:53682/callback"},
|
|
requested: "http://127.0.0.1:8080/callback",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "loopback host must match (localhost vs 127.0.0.1)",
|
|
registered: []uri.URI{"http://127.0.0.1/callback"},
|
|
requested: "http://localhost:3118/callback",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "loopback path must match",
|
|
registered: []uri.URI{"http://localhost/callback"},
|
|
requested: "http://localhost:3118/other",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "loopback scheme must match",
|
|
registered: []uri.URI{"http://localhost/callback"},
|
|
requested: "https://localhost:3118/callback",
|
|
want: false,
|
|
},
|
|
{
|
|
name: "non-loopback host does not get port flexibility",
|
|
registered: []uri.URI{"https://app.example.com/callback"},
|
|
requested: "https://app.example.com:8443/callback",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(
|
|
tt.name,
|
|
func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := &coredata.OAuth2Client{RedirectURIs: tt.registered}
|
|
|
|
assert.Equal(t, tt.want, client.IsRedirectURIAllowed(tt.requested))
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestNewCIMDClient_WebURIs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run(
|
|
"accepts https client_uri and logo_uri",
|
|
func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
clientURI := "https://mcp.example.com"
|
|
logoURI := "https://mcp.example.com/logo.png"
|
|
|
|
client, err := coredata.NewCIMDClient(
|
|
"https://mcp.example.com/oauth/metadata.json",
|
|
"Example MCP",
|
|
[]string{"https://mcp.example.com/callback"},
|
|
nil,
|
|
&logoURI,
|
|
&clientURI,
|
|
time.Now(),
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, client.ClientURI)
|
|
require.NotNil(t, client.LogoURI)
|
|
assert.Equal(t, "https://mcp.example.com", client.ClientURI.String())
|
|
assert.Equal(t, "https://mcp.example.com/logo.png", client.LogoURI.String())
|
|
},
|
|
)
|
|
|
|
t.Run(
|
|
"rejects non-web client_uri",
|
|
func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
clientURI := "javascript://example.com/%0Aalert(1)"
|
|
|
|
_, err := coredata.NewCIMDClient(
|
|
"https://mcp.example.com/oauth/metadata.json",
|
|
"Example MCP",
|
|
[]string{"https://mcp.example.com/callback"},
|
|
nil,
|
|
nil,
|
|
&clientURI,
|
|
time.Now(),
|
|
)
|
|
require.Error(t, err)
|
|
},
|
|
)
|
|
|
|
t.Run(
|
|
"rejects non-web logo_uri",
|
|
func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logoURI := "data://example.com/image"
|
|
|
|
_, err := coredata.NewCIMDClient(
|
|
"https://mcp.example.com/oauth/metadata.json",
|
|
"Example MCP",
|
|
[]string{"https://mcp.example.com/callback"},
|
|
nil,
|
|
&logoURI,
|
|
nil,
|
|
time.Now(),
|
|
)
|
|
require.Error(t, err)
|
|
},
|
|
)
|
|
}
|