The CORS middleware let every origin make credentialed requests, so a page on another subdomain of the server's domain could read and act as the signed-in user. Nothing needs cross-origin access, so the middleware and the go-chi/cors dependency go. A test checks that a request from another origin and a preflight get no Access-Control-Allow-* headers. Model: opus-5-5
This commit is contained in:
1 parent
f42c6214a0
commit
3587ce92cd
6 files changed
+74
-39
No files matched your search
@@ -168,8 +168,8 @@ either.
|
|||||||
- Request bodies for `POST /api/v1/session`, `POST /api/v1/login` and
|
- Request bodies for `POST /api/v1/session`, `POST /api/v1/login` and
|
||||||
`POST /api/v1/messages` are limited to `MAX_MESSAGE_SIZE` bytes (default
|
`POST /api/v1/messages` are limited to `MAX_MESSAGE_SIZE` bytes (default
|
||||||
`4096`). A larger body gets `400` `invalid request body`.
|
`4096`). A larger body gets `400` `invalid request body`.
|
||||||
- The server allows cross-origin requests from any origin, with credentials. The
|
- The server allows no cross-origin requests. In a browser, only pages the
|
||||||
cookie is `SameSite=Strict`, so a browser sends it only from the same site.
|
server itself serves, such as the web client, can use the API.
|
||||||
|
|
||||||
### The auth cookie
|
### The auth cookie
|
||||||
|
|
||||||
@@ -1892,14 +1892,13 @@ The IRC listener has no TLS.
|
|||||||
seconds. A request still running after 60 seconds is cancelled, and the
|
seconds. A request still running after 60 seconds is cancelled, and the
|
||||||
connection closes without a response. Headers may be up to 1 MiB.
|
connection closes without a response. Headers may be up to 1 MiB.
|
||||||
- A long poll waits at most 30 seconds.
|
- A long poll waits at most 30 seconds.
|
||||||
- Every response to a well-formed request, except to a cross-origin preflight,
|
- Every response to a well-formed request carries a `Content-Security-Policy`
|
||||||
carries a `Content-Security-Policy` header:
|
header:
|
||||||
`default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`.
|
`default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self'; font-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'`.
|
||||||
The web client works within it.
|
The web client works within it.
|
||||||
- Cross-origin requests are allowed from any origin, with credentials. The
|
- The server allows no cross-origin requests. No response carries an
|
||||||
allowed methods are `GET`, `POST`, `PUT`, `DELETE` and `OPTIONS`. The allowed
|
`Access-Control-Allow-*` header. A preflight request gets `405`, like any
|
||||||
request headers are `Accept`, `Content-Type` and `X-CSRF-Token`. The cookie's
|
other `OPTIONS` request.
|
||||||
`SameSite=Strict` still keeps it to the same site.
|
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
@@ -2002,8 +2001,8 @@ and their sessions to end. Then it closes the database.
|
|||||||
gets a message.
|
gets a message.
|
||||||
- `internal/hashcash/`: stamp checking and minting.
|
- `internal/hashcash/`: stamp checking and minting.
|
||||||
- `internal/ratelimit/`: the per-address login rate limiter.
|
- `internal/ratelimit/`: the per-address login rate limiter.
|
||||||
- `internal/middleware/`: finding the client's address, request logging,
|
- `internal/middleware/`: finding the client's address, request logging, the
|
||||||
cross-origin requests, the `Content-Security-Policy` header and metrics.
|
`Content-Security-Policy` header and metrics.
|
||||||
- `internal/healthcheck/` and `internal/stats/`: the health check and its
|
- `internal/healthcheck/` and `internal/stats/`: the health check and its
|
||||||
counters.
|
counters.
|
||||||
- `internal/logger/` and `internal/globals/`: logging setup and build
|
- `internal/logger/` and `internal/globals/`: logging setup and build
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ require (
|
|||||||
github.com/gdamore/tcell/v2 v2.13.8
|
github.com/gdamore/tcell/v2 v2.13.8
|
||||||
github.com/getsentry/sentry-go v0.42.0
|
github.com/getsentry/sentry-go v0.42.0
|
||||||
github.com/go-chi/chi/v5 v5.2.1
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
github.com/go-chi/cors v1.2.2
|
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7
|
|||||||
github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
|
|
||||||
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
|
||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
basicauth "github.com/99designs/basicauth-go"
|
basicauth "github.com/99designs/basicauth-go"
|
||||||
chimw "github.com/go-chi/chi/v5/middleware"
|
chimw "github.com/go-chi/chi/v5/middleware"
|
||||||
"github.com/go-chi/cors"
|
|
||||||
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
metrics "github.com/slok/go-http-metrics/metrics/prometheus"
|
||||||
ghmm "github.com/slok/go-http-metrics/middleware"
|
ghmm "github.com/slok/go-http-metrics/middleware"
|
||||||
"github.com/slok/go-http-metrics/middleware/std"
|
"github.com/slok/go-http-metrics/middleware/std"
|
||||||
@@ -24,8 +23,6 @@ import (
|
|||||||
"sneak.berlin/go/neoirc/internal/logger"
|
"sneak.berlin/go/neoirc/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
const corsMaxAge = 300
|
|
||||||
|
|
||||||
// Params defines the dependencies for creating Middleware.
|
// Params defines the dependencies for creating Middleware.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
fx.In
|
fx.In
|
||||||
@@ -200,28 +197,6 @@ func (mware *Middleware) Logging() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS returns middleware that handles Cross-Origin Resource Sharing.
|
|
||||||
// AllowCredentials is true so browsers include cookies in
|
|
||||||
// cross-origin API requests.
|
|
||||||
func (mware *Middleware) CORS() func(http.Handler) http.Handler {
|
|
||||||
return cors.Handler(cors.Options{ //nolint:exhaustruct // optional fields
|
|
||||||
AllowOriginFunc: func(
|
|
||||||
_ *http.Request, _ string,
|
|
||||||
) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
AllowedMethods: []string{
|
|
||||||
"GET", "POST", "PUT", "DELETE", "OPTIONS",
|
|
||||||
},
|
|
||||||
AllowedHeaders: []string{
|
|
||||||
"Accept", "Content-Type", "X-CSRF-Token",
|
|
||||||
},
|
|
||||||
ExposedHeaders: []string{"Link"},
|
|
||||||
AllowCredentials: true,
|
|
||||||
MaxAge: corsMaxAge,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metrics returns middleware that records HTTP metrics.
|
// Metrics returns middleware that records HTTP metrics.
|
||||||
func (mware *Middleware) Metrics() func(http.Handler) http.Handler {
|
func (mware *Middleware) Metrics() func(http.Handler) http.Handler {
|
||||||
metricsMiddleware := ghmm.New(ghmm.Config{ //nolint:exhaustruct // optional fields
|
metricsMiddleware := ghmm.New(ghmm.Config{ //nolint:exhaustruct // optional fields
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ func (srv *Server) SetupRoutes() {
|
|||||||
srv.router.Use(srv.mw.Metrics())
|
srv.router.Use(srv.mw.Metrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
srv.router.Use(srv.mw.CORS())
|
|
||||||
srv.router.Use(srv.mw.CSP())
|
srv.router.Use(srv.mw.CSP())
|
||||||
srv.router.Use(middleware.Timeout(routeTimeout))
|
srv.router.Use(middleware.Timeout(routeTimeout))
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -188,6 +189,70 @@ func TestServeAfterStopDoesNotStopApp(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNoCrossOriginAccess checks that the server allows no
|
||||||
|
// cross-origin requests: neither a request from another origin
|
||||||
|
// nor a preflight gets Access-Control-Allow-Origin or
|
||||||
|
// Access-Control-Allow-Credentials, and a preflight gets 405
|
||||||
|
// like any other OPTIONS request.
|
||||||
|
func TestNoCrossOriginAccess(t *testing.T) {
|
||||||
|
var srv *server.Server
|
||||||
|
|
||||||
|
startApp(t, fx.Populate(&srv))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
requestMethod string
|
||||||
|
status int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "request",
|
||||||
|
method: http.MethodGet,
|
||||||
|
status: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preflight",
|
||||||
|
method: http.MethodOptions,
|
||||||
|
requestMethod: http.MethodPost,
|
||||||
|
status: http.StatusMethodNotAllowed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
request := httptest.NewRequestWithContext(
|
||||||
|
t.Context(), test.method,
|
||||||
|
"http://chat.example.com/api/v1/server", nil,
|
||||||
|
)
|
||||||
|
request.Header.Set("Origin", "http://blog.example.com")
|
||||||
|
|
||||||
|
if test.requestMethod != "" {
|
||||||
|
request.Header.Set(
|
||||||
|
"Access-Control-Request-Method", test.requestMethod,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
srv.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != test.status {
|
||||||
|
t.Errorf("status %d, want %d", recorder.Code, test.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, header := range []string{
|
||||||
|
"Access-Control-Allow-Origin",
|
||||||
|
"Access-Control-Allow-Credentials",
|
||||||
|
} {
|
||||||
|
value := recorder.Header().Get(header)
|
||||||
|
if value != "" {
|
||||||
|
t.Errorf("%s is %q, want none", header, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// startApp starts the app newApp builds, with the HTTP port
|
// startApp starts the app newApp builds, with the HTTP port
|
||||||
// set to 0, so the system gives the server a free port.
|
// set to 0, so the system gives the server a free port.
|
||||||
func startApp(
|
func startApp(
|
||||||
|
|||||||
Reference in new issue
Block a user