mcp: verify 'Origin' and 'Content-Type' headers (#842)
This PR introduces protection against cross-origin requests by installing `http.CrossOriginProtection`. By default, the zero value is used which does not contain any trusted origins or bypass patterns. The protection can be customized by providing a custom `http.CrossOriginProtection` in `mcp.StreamableHTTPOptions`. Currently, the deny handler set on the protection is ignored. This default has an MCPGODEBUG option (`disableoriginverification`) to disable the default protection. It will be available until `v1.6.0` of the SDK. We also increase the Go version required by the SDK to 1.25, since this is the version that introduced `http.CrossOriginProtection`. This is in line with the SDK's policy of supporting two newest Go versions (1.26 was released in February 2026). Additionally, we start validating if the `Content-Type` header for `POST` requests is set to `application/json` to avoid accepting CORS-safelisted requests.
This commit is contained in:
parent
421ddf1ef2
commit
580f2a09be
15 changed files with 252 additions and 11 deletions
3
.github/workflows/codeql.yml
vendored
3
.github/workflows/codeql.yml
vendored
|
|
@ -8,6 +8,9 @@ on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '31 9 * * 4'
|
- cron: '31 9 * * 4'
|
||||||
|
|
||||||
|
# Declare default permissions as read only.
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze (${{ matrix.language }})
|
name: Analyze (${{ matrix.language }})
|
||||||
|
|
|
||||||
4
.github/workflows/conformance.yml
vendored
4
.github/workflows/conformance.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: "^1.25"
|
go-version: "^1.26"
|
||||||
- name: Start everything-server
|
- name: Start everything-server
|
||||||
run: |
|
run: |
|
||||||
go run ./conformance/everything-server/main.go -http=":3001" &
|
go run ./conformance/everything-server/main.go -http=":3001" &
|
||||||
|
|
@ -45,7 +45,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: "^1.25"
|
go-version: "^1.26"
|
||||||
- name: "Run conformance tests"
|
- name: "Run conformance tests"
|
||||||
uses: modelcontextprotocol/conformance@a2855b03582a6c0b31065ad4d9af248316ce61a3 # v0.1.15
|
uses: modelcontextprotocol/conformance@a2855b03582a6c0b31065ad4d9af248316ce61a3 # v0.1.15
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
2
.github/workflows/docs-check.yml
vendored
2
.github/workflows/docs-check.yml
vendored
|
|
@ -17,6 +17,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
|
with:
|
||||||
|
go-version: "^1.26"
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Check docs are up-to-date
|
- name: Check docs are up-to-date
|
||||||
|
|
|
||||||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: "^1.25"
|
go-version: "^1.26"
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
|
|
@ -40,7 +40,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go: ["1.24", "1.25"]
|
go: ["1.25", "1.26"]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
@ -52,6 +52,8 @@ jobs:
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
race-test:
|
race-test:
|
||||||
|
# Temporarily disable until fixes are prepared.
|
||||||
|
if: false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
|
|
@ -59,6 +61,6 @@ jobs:
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version: "1.25"
|
||||||
- name: Test with -race
|
- name: Test with -race
|
||||||
run: go test -v -race ./...
|
run: go test -v -race ./...
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,11 @@ protocol.
|
||||||
|
|
||||||
See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
|
See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
|
||||||
|
|
||||||
|
# Backwards compatibility
|
||||||
|
|
||||||
|
See [mcpgodebug.md](mcpgodebug.md) for a list of backwards incompatible behavior changes
|
||||||
|
and description how they can be temporarily undone.
|
||||||
|
|
||||||
# Rough edges
|
# Rough edges
|
||||||
|
|
||||||
See [rough_edges.md](rough_edges.md) for a list of rough edges or API
|
See [rough_edges.md](rough_edges.md) for a list of rough edges or API
|
||||||
|
|
|
||||||
41
docs/mcpgodebug.md
Normal file
41
docs/mcpgodebug.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!-- Autogenerated by weave; DO NOT EDIT -->
|
||||||
|
# Backwards compatibility and MCPGODEBUG
|
||||||
|
|
||||||
|
According to our compatibility promise, we can't break backward compatibility
|
||||||
|
of the SDK API. However, sometimes we need to change the behavior of the SDK
|
||||||
|
in a backward-incompatible way in order to fix bugs or security issues.
|
||||||
|
In those cases we introduce temporary compatibility parameters, that can be
|
||||||
|
used to opt-out of the new behavior. They are usually maintained for two
|
||||||
|
minor release cycles and then removed.
|
||||||
|
|
||||||
|
The compatibility parameters are provided via the `MCPGODEBUG` environment
|
||||||
|
variable. The value of the variable is a comma-separated list of parameter
|
||||||
|
value assignments, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
MCPGODEBUG=parameter1=value1,parameter2=value2
|
||||||
|
```
|
||||||
|
|
||||||
|
## `MCPGODEBUG` history
|
||||||
|
|
||||||
|
### 1.4.1
|
||||||
|
|
||||||
|
Options listed below will be removed in the 1.6.0 version of the SDK.
|
||||||
|
|
||||||
|
- `disablecrossoriginprotection` added. If set to `1`, newly added cross-origin
|
||||||
|
protection will be disabled. The default behavior was changed to enable
|
||||||
|
cross-origin protection.
|
||||||
|
|
||||||
|
### 1.4.0
|
||||||
|
|
||||||
|
Options listed below will be removed in the 1.6.0 version of the SDK.
|
||||||
|
|
||||||
|
- `jsonescaping` added. If set to `1`, JSON marshaling will preserve the previous
|
||||||
|
behavior of escaping HTML characters in JSON strings. The default behavior
|
||||||
|
was changed to not escape HTML characters, to be consistent with other SDKs.
|
||||||
|
|
||||||
|
- `disablelocalhostprotection` added. If set to `1`, newly added DNS rebinding
|
||||||
|
protection will be disabled. The default behavior was changed to enable DNS rebinding
|
||||||
|
protection. The protection can also be disabled by setting the
|
||||||
|
`DisableLocalhostProtection` field in the `StreamableHTTPOptions` struct to
|
||||||
|
`true`, which is the recommended way to disable the protection long term.
|
||||||
|
|
@ -360,7 +360,7 @@ and step-up authentication (when the server returns `insufficient_scope` error).
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Here we discuss the mitigations described under
|
Here we discuss the mitigations described under
|
||||||
the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) section, and how we handle them.
|
the MCP's [Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) section, and how we handle them.
|
||||||
|
|
||||||
### Confused Deputy
|
### Confused Deputy
|
||||||
|
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/modelcontextprotocol/go-sdk
|
module github.com/modelcontextprotocol/go-sdk
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,11 @@ protocol.
|
||||||
|
|
||||||
See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
|
See [troubleshooting.md](troubleshooting.md) for a troubleshooting guide.
|
||||||
|
|
||||||
|
# Backwards compatibility
|
||||||
|
|
||||||
|
See [mcpgodebug.md](mcpgodebug.md) for a list of backwards incompatible behavior changes
|
||||||
|
and description how they can be temporarily undone.
|
||||||
|
|
||||||
# Rough edges
|
# Rough edges
|
||||||
|
|
||||||
See [rough_edges.md](rough_edges.md) for a list of rough edges or API
|
See [rough_edges.md](rough_edges.md) for a list of rough edges or API
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
//go:generate weave -o ../../docs/server.md ./server.src.md
|
//go:generate weave -o ../../docs/server.md ./server.src.md
|
||||||
//go:generate weave -o ../../docs/troubleshooting.md ./troubleshooting.src.md
|
//go:generate weave -o ../../docs/troubleshooting.md ./troubleshooting.src.md
|
||||||
//go:generate weave -o ../../docs/rough_edges.md ./rough_edges.src.md
|
//go:generate weave -o ../../docs/rough_edges.md ./rough_edges.src.md
|
||||||
|
//go:generate weave -o ../../docs/mcpgodebug.md ./mcpgodebug.src.md
|
||||||
|
|
||||||
// The doc package generates the documentation at /doc, via go:generate.
|
// The doc package generates the documentation at /doc, via go:generate.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
40
internal/docs/mcpgodebug.src.md
Normal file
40
internal/docs/mcpgodebug.src.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Backwards compatibility and MCPGODEBUG
|
||||||
|
|
||||||
|
According to our compatibility promise, we can't break backward compatibility
|
||||||
|
of the SDK API. However, sometimes we need to change the behavior of the SDK
|
||||||
|
in a backward-incompatible way in order to fix bugs or security issues.
|
||||||
|
In those cases we introduce temporary compatibility parameters, that can be
|
||||||
|
used to opt-out of the new behavior. They are usually maintained for two
|
||||||
|
minor release cycles and then removed.
|
||||||
|
|
||||||
|
The compatibility parameters are provided via the `MCPGODEBUG` environment
|
||||||
|
variable. The value of the variable is a comma-separated list of parameter
|
||||||
|
value assignments, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
MCPGODEBUG=parameter1=value1,parameter2=value2
|
||||||
|
```
|
||||||
|
|
||||||
|
## `MCPGODEBUG` history
|
||||||
|
|
||||||
|
### 1.4.1
|
||||||
|
|
||||||
|
Options listed below will be removed in the 1.6.0 version of the SDK.
|
||||||
|
|
||||||
|
- `disablecrossoriginprotection` added. If set to `1`, newly added cross-origin
|
||||||
|
protection will be disabled. The default behavior was changed to enable
|
||||||
|
cross-origin protection.
|
||||||
|
|
||||||
|
### 1.4.0
|
||||||
|
|
||||||
|
Options listed below will be removed in the 1.6.0 version of the SDK.
|
||||||
|
|
||||||
|
- `jsonescaping` added. If set to `1`, JSON marshaling will preserve the previous
|
||||||
|
behavior of escaping HTML characters in JSON strings. The default behavior
|
||||||
|
was changed to not escape HTML characters, to be consistent with other SDKs.
|
||||||
|
|
||||||
|
- `disablelocalhostprotection` added. If set to `1`, newly added DNS rebinding
|
||||||
|
protection will be disabled. The default behavior was changed to enable DNS rebinding
|
||||||
|
protection. The protection can also be disabled by setting the
|
||||||
|
`DisableLocalhostProtection` field in the `StreamableHTTPOptions` struct to
|
||||||
|
`true`, which is the recommended way to disable the protection long term.
|
||||||
|
|
@ -285,7 +285,7 @@ and step-up authentication (when the server returns `insufficient_scope` error).
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
Here we discuss the mitigations described under
|
Here we discuss the mitigations described under
|
||||||
the MCP spec's [Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) section, and how we handle them.
|
the MCP's [Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) section, and how we handle them.
|
||||||
|
|
||||||
### Confused Deputy
|
### Confused Deputy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,14 @@ type StreamableHTTPOptions struct {
|
||||||
// Only disable this if you understand the security implications.
|
// Only disable this if you understand the security implications.
|
||||||
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
|
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
|
||||||
DisableLocalhostProtection bool
|
DisableLocalhostProtection bool
|
||||||
|
|
||||||
|
// CrossOriginProtection allows to customize cross-origin protection.
|
||||||
|
// The deny handler set in the CrossOriginProtection through SetDenyHandler
|
||||||
|
// is ignored.
|
||||||
|
// If nil, default (zero-value) cross-origin protection will be used.
|
||||||
|
// Use `disablecrossoriginprotection` MCPGODEBUG compatibility parameter
|
||||||
|
// to disable the default protection until v1.6.0.
|
||||||
|
CrossOriginProtection *http.CrossOriginProtection
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStreamableHTTPHandler returns a new [StreamableHTTPHandler].
|
// NewStreamableHTTPHandler returns a new [StreamableHTTPHandler].
|
||||||
|
|
@ -190,8 +198,10 @@ func NewStreamableHTTPHandler(getServer func(*http.Request) *Server, opts *Strea
|
||||||
h.opts = *opts
|
h.opts = *opts
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.opts.Logger == nil { // ensure we have a logger
|
h.opts.Logger = ensureLogger(h.opts.Logger)
|
||||||
h.opts.Logger = ensureLogger(nil)
|
|
||||||
|
if h.opts.CrossOriginProtection == nil {
|
||||||
|
h.opts.CrossOriginProtection = &http.CrossOriginProtection{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
@ -226,6 +236,13 @@ func (h *StreamableHTTPHandler) closeAll() {
|
||||||
// The option will be removed in the 1.6.0 version of the SDK.
|
// The option will be removed in the 1.6.0 version of the SDK.
|
||||||
var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")
|
var disablelocalhostprotection = mcpgodebug.Value("disablelocalhostprotection")
|
||||||
|
|
||||||
|
// disablecrossoriginprotection is a compatibility parameter that allows to disable
|
||||||
|
// the verification of the 'Origin' and 'Content-Type' headers, which was added in
|
||||||
|
// the 1.4.1 version of the SDK. See the documentation for the mcpgodebug package
|
||||||
|
// for instructions how to enable it.
|
||||||
|
// The option will be removed in the 1.6.0 version of the SDK.
|
||||||
|
var disablecrossoriginprotection = mcpgodebug.Value("disablecrossoriginprotection")
|
||||||
|
|
||||||
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
// DNS rebinding protection: auto-enabled for localhost servers.
|
// DNS rebinding protection: auto-enabled for localhost servers.
|
||||||
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
|
// See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise
|
||||||
|
|
@ -238,6 +255,22 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if disablecrossoriginprotection != "1" {
|
||||||
|
// Verify the 'Origin' header to protect against CSRF attacks.
|
||||||
|
if err := h.opts.CrossOriginProtection.Check(req); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Validate 'Content-Type' header.
|
||||||
|
if req.Method == http.MethodPost {
|
||||||
|
contentType := req.Header.Get("Content-Type")
|
||||||
|
if contentType != "application/json" {
|
||||||
|
http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Allow multiple 'Accept' headers.
|
// Allow multiple 'Accept' headers.
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax
|
||||||
accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
|
accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",")
|
||||||
|
|
|
||||||
|
|
@ -832,6 +832,36 @@ func TestStreamableServerTransport(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantSessions: 0,
|
wantSessions: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "content type headers",
|
||||||
|
requests: []streamableRequest{
|
||||||
|
initialize,
|
||||||
|
initialized,
|
||||||
|
{
|
||||||
|
// Request with incorrect Content-Type should be rejected.
|
||||||
|
method: "POST",
|
||||||
|
headers: http.Header{"Content-Type": {"text/plain"}},
|
||||||
|
messages: []jsonrpc.Message{req(3, "tools/call", &CallToolParams{Name: "tool"})},
|
||||||
|
wantStatusCode: http.StatusUnsupportedMediaType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Request with empty Content-Type should be rejected.
|
||||||
|
method: "POST",
|
||||||
|
headers: http.Header{"Content-Type": {""}},
|
||||||
|
messages: []jsonrpc.Message{req(4, "tools/call", &CallToolParams{Name: "tool"})},
|
||||||
|
wantStatusCode: http.StatusUnsupportedMediaType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Correct Content-Type should pass.
|
||||||
|
method: "POST",
|
||||||
|
headers: http.Header{"Content-Type": {"application/json"}},
|
||||||
|
messages: []jsonrpc.Message{req(5, "tools/call", &CallToolParams{Name: "tool"})},
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
wantMessages: []jsonrpc.Message{resp(5, &CallToolResult{Content: []Content{}}, nil)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSessions: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "accept headers",
|
name: "accept headers",
|
||||||
requests: []streamableRequest{
|
requests: []streamableRequest{
|
||||||
|
|
@ -1409,10 +1439,16 @@ func (s streamableRequest) do(ctx context.Context, serverURL, sessionID string,
|
||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
req.Header.Set(sessionIDHeader, sessionID)
|
req.Header.Set(sessionIDHeader, sessionID)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
if s.method == http.MethodPost {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
req.Header.Set("Accept", "application/json, text/event-stream")
|
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||||
maps.Copy(req.Header, s.headers)
|
maps.Copy(req.Header, s.headers)
|
||||||
|
|
||||||
|
if req.Header.Get("Content-Type") == "" {
|
||||||
|
req.Header.Del("Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, nil, fmt.Errorf("request failed: %v", err)
|
return "", 0, nil, fmt.Errorf("request failed: %v", err)
|
||||||
|
|
@ -2436,3 +2472,76 @@ func TestStreamableLocalhostProtection(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStreamableOriginProtection(t *testing.T) {
|
||||||
|
server := NewServer(testImpl, nil)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
protection *http.CrossOriginProtection
|
||||||
|
requestOrigin string
|
||||||
|
wantStatusCode int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default protection with Origin header",
|
||||||
|
protection: nil,
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom protection with trusted origin and same Origin",
|
||||||
|
protection: func() *http.CrossOriginProtection {
|
||||||
|
p := http.NewCrossOriginProtection()
|
||||||
|
if err := p.AddTrustedOrigin("https://example.com"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
requestOrigin: "https://example.com",
|
||||||
|
wantStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom protection with trusted origin and different Origin",
|
||||||
|
protection: func() *http.CrossOriginProtection {
|
||||||
|
p := http.NewCrossOriginProtection()
|
||||||
|
if err := p.AddTrustedOrigin("https://example.com"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}(),
|
||||||
|
requestOrigin: "https://malicious.com",
|
||||||
|
wantStatusCode: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
opts := &StreamableHTTPOptions{
|
||||||
|
Stateless: true, // avoid session ID requirement
|
||||||
|
CrossOriginProtection: tt.protection,
|
||||||
|
}
|
||||||
|
handler := NewStreamableHTTPHandler(func(req *http.Request) *Server { return server }, opts)
|
||||||
|
httpServer := httptest.NewServer(handler)
|
||||||
|
defer httpServer.Close()
|
||||||
|
|
||||||
|
reqReader := strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}`)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, httpServer.URL, reqReader)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Origin", tt.requestOrigin)
|
||||||
|
req.Header.Set("Accept", "application/json, text/event-stream")
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if got := resp.StatusCode; got != tt.wantStatusCode {
|
||||||
|
t.Errorf("Status code: got %d, want %d", got, tt.wantStatusCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue