Merge pull request #117 from safing/feature/api-security-headers

Add security headers to API
This commit is contained in:
Patrick Pacher 2021-01-29 14:45:48 +01:00 committed by GitHub
commit 7364562211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 92 additions and 97 deletions

View file

@ -133,22 +133,6 @@ func SetAuthenticator(fn AuthenticatorFunc) error {
return nil
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := authenticateRequest(w, r, next)
if token == nil {
// Authenticator already replied.
return
}
// Add token to request and serve next handler.
if _, apiRequest := getAPIContext(r); apiRequest != nil {
apiRequest.AuthToken = token
}
next.ServeHTTP(w, r)
})
}
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
tracer := log.Tracer(r.Context())
@ -258,6 +242,14 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler h
}
func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
// Return highest possible permissions in dev mode.
if devMode() {
return &AuthToken{
Read: PermitSelf,
Write: PermitSelf,
}, false
}
// Check for valid API key.
token = checkAPIKey(r)
if token != nil {
@ -478,7 +470,12 @@ func deleteSession(sessionKey string) {
}
func isReadMethod(method string) bool {
return method == http.MethodGet || method == http.MethodHead
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
return true
default:
return false
}
}
func parseAPIPermission(s string) (Permission, error) {

View file

@ -1,6 +1,6 @@
package client
// message types
// Message Types.
const (
msgRequestGet = "get"
msgRequestQuery = "query"

View file

@ -9,7 +9,7 @@ import (
"github.com/tevino/abool"
)
// Client errors
// Client errors.
var (
ErrMalformedMessage = errors.New("malformed message")
)

View file

@ -7,7 +7,7 @@ import (
"github.com/safing/portbase/log"
)
// Config Keys
// Config Keys.
const (
CfgDefaultListenAddressKey = "core/listenAddress"
CfgAPIKeys = "core/apiKeys"
@ -19,6 +19,8 @@ var (
defaultListenAddress string
configuredAPIKeys config.StringArrayOption
devMode config.BoolOption
)
func init() {
@ -79,6 +81,8 @@ func registerConfig() error {
}
configuredAPIKeys = config.GetAsStringArray(CfgAPIKeys, []string{})
devMode = config.Concurrent.GetAsBool(config.CfgDevModeKey, false)
return nil
}

View file

@ -70,7 +70,7 @@ type (
RecordFunc func(ar *Request) (r record.Record, err error)
)
// MIME Types
// MIME Types.
const (
MimeTypeJSON string = "application/json"
MimeTypeText string = "text/plain"
@ -256,6 +256,9 @@ func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
apiRequest.InputData = inputData
case http.MethodGet:
// Nothing special to do here.
case http.MethodOptions:
w.WriteHeader(http.StatusNoContent)
return
default:
http.Error(w, "Unsupported method for the actions API.", http.StatusMethodNotAllowed)
return
@ -308,7 +311,6 @@ func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Write response.
w.Header().Set("Content-Type", apiEndpoint.MimeType+"; charset=utf-8")
w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
w.Header().Set("X-Content-Type-Options", "nosniff")
w.WriteHeader(http.StatusOK)
_, err = w.Write(responseData)
if err != nil {

View file

@ -17,7 +17,7 @@ var (
exportEndpoints bool
)
// API Errors
// API Errors.
var (
ErrAuthenticationAlreadySet = errors.New("the authentication function has already been set")
ErrAuthenticationImmutable = errors.New("the authentication function can only be set before the api has started")

View file

@ -1,52 +0,0 @@
package api
import (
"context"
"net/http"
"github.com/safing/portbase/log"
)
// Middleware is a function that can be added as a middleware to the API endpoint.
type Middleware func(next http.Handler) http.Handler
type mwHandler struct {
handlers []Middleware
final http.Handler
}
func (mwh *mwHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handlerLock.RLock()
defer handlerLock.RUnlock()
// final handler
handler := mwh.final
// build middleware chain
// loop in reverse to build the handler chain in the correct order
for i := len(mwh.handlers) - 1; i >= 0; i-- {
handler = mwh.handlers[i](handler)
}
// start
handler.ServeHTTP(w, r)
}
// ModuleWorker is an http middleware that wraps the request in a module worker.
func ModuleWorker(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = module.RunWorker("http request", func(_ context.Context) error {
next.ServeHTTP(w, r)
return nil
})
})
}
// LogTracer is an http middleware that attaches a log tracer to the request context.
func LogTracer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, tracer := log.AddTracer(r.Context())
next.ServeHTTP(w, r.WithContext(ctx))
tracer.Submit()
})
}

View file

@ -16,17 +16,6 @@ var (
// gorilla mux
mainMux = mux.NewRouter()
// middlewares
middlewareHandler = &mwHandler{
final: mainMux,
handlers: []Middleware{
ModuleWorker,
LogTracer,
RequestLogger,
authMiddleware,
},
}
// main server and lock
server = &http.Server{}
handlerLock sync.RWMutex
@ -46,18 +35,12 @@ func RegisterHandleFunc(path string, handleFunc func(http.ResponseWriter, *http.
return mainMux.HandleFunc(path, handleFunc)
}
// RegisterMiddleware registers a middle function with the API endoint.
func RegisterMiddleware(middleware Middleware) {
handlerLock.Lock()
defer handlerLock.Unlock()
middlewareHandler.handlers = append(middlewareHandler.handlers, middleware)
}
// Serve starts serving the API endpoint.
func Serve() {
// configure server
server.Addr = listenAddressConfig()
server.Handler = &mainHandler{
// TODO: mainMux should not be modified anymore.
mux: mainMux,
}
@ -134,6 +117,22 @@ func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
return nil
}
// Add security headers.
if !devMode() {
w.Header().Set(
"Content-Security-Policy",
"default-src 'self'; "+
"style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data:",
)
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "1; mode=block")
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
// Handle request.
switch {
case handler != nil:

39
config/devmode.go Normal file
View file

@ -0,0 +1,39 @@
package config
import (
"flag"
"github.com/safing/portbase/log"
)
// Configuration Keys.
var (
CfgDevModeKey = "core/devMode"
defaultDevMode bool
)
func init() {
flag.BoolVar(&defaultDevMode, "devmode", false, "enable development mode")
}
func logDevModeOverride() {
if defaultDevMode {
log.Warning("config: development mode is enabled by default by the -devmode flag")
}
}
func registerDevModeOption() error {
return Register(&Option{
Name: "Development Mode",
Key: CfgDevModeKey,
Description: "In Development Mode, security restrictions are lifted/softened to enable unrestricted access for debugging and testing purposes.",
OptType: OptTypeBool,
ExpertiseLevel: ExpertiseLevelDeveloper,
ReleaseLevel: ReleaseLevelStable,
DefaultValue: defaultDevMode,
Annotations: Annotations{
DisplayOrderAnnotation: 512,
CategoryAnnotation: "Development",
},
})
}

View file

@ -14,7 +14,7 @@ import (
// to change deep configuration settings.
type ExpertiseLevel uint8
// Expertise Level constants
// Expertise Level constants.
const (
ExpertiseLevelUser ExpertiseLevel = 0
ExpertiseLevelExpert ExpertiseLevel = 1

View file

@ -47,6 +47,12 @@ func prep() error {
modules.SetCmdLineOperation(exportConfigCmd)
}
logDevModeOverride()
err := registerDevModeOption()
if err != nil {
return err
}
return nil
}

View file

@ -60,7 +60,7 @@ type PossibleValue struct {
// future well-known annotation additions do not conflict
// with vendor/product/package specific annoations.
//
// Format: <vendor/package>:<scope>:<identifier>
// Format: <vendor/package>:<scope>:<identifier> //.
type Annotations map[string]interface{}
// Well known annotations defined by this package.
@ -144,7 +144,7 @@ type ValueRequirement struct {
Value interface{}
}
// Values for the DisplayHintAnnotation
// Values for the DisplayHintAnnotation.
const (
// DisplayHintOneOf is used to mark an option
// as a "select"-style option. That is, only one of
@ -263,7 +263,7 @@ func (option *Option) SetAnnotation(key string, value interface{}) {
option.Annotations[key] = value
}
// GetAnnotation returns the value of the annotation key
// GetAnnotation returns the value of the annotation key.
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
option.Lock()
defer option.Unlock()