From b12d9d405fc83b68094ae9d6f22753eae4b8917e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Michaud?= Date: Tue, 21 Apr 2020 02:23:28 +0200 Subject: [PATCH] [FEATURE] Add Content-Security-Policy meta to login portal. (#822) CSP is used to avoid some attacks where the hacker tries to execute untrusted code in the browser. The policy is to use assets hosted on the the original website and in order to make CSP work with material UI, a nonce is generated at each request of index.html and injected in the template as well as provided in the Content-Security-Policy header (https://material-ui.com/styles/advanced/#how-does-one-implement-csp) Fix #815 --- Dockerfile | 2 +- Dockerfile.arm32v7 | 2 +- Dockerfile.arm64v8 | 2 +- Dockerfile.darwin | 2 +- cmd/authelia-scripts/cmd_build.go | 1 + internal/server/index.go | 46 +++++++++++++++++++ internal/server/server.go | 3 +- .../authelia/docker-compose.backend.dev.yml | 1 + .../authelia/resources/entrypoint-backend.sh | 5 ++ .../compose/authelia/resources/nginx.conf | 4 ++ web/public/index.html | 2 + 11 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 internal/server/index.go diff --git a/Dockerfile b/Dockerfile index f6b2581fb..16d797f6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,7 @@ WORKDIR /node/src/app COPY web . # Install the dependencies and build -RUN yarn install --frozen-lockfile && yarn build +RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build # =================================== # ===== Authelia official image ===== diff --git a/Dockerfile.arm32v7 b/Dockerfile.arm32v7 index ca2386ca0..8cccffd3a 100644 --- a/Dockerfile.arm32v7 +++ b/Dockerfile.arm32v7 @@ -39,7 +39,7 @@ WORKDIR /node/src/app COPY web . # Install the dependencies and build -RUN yarn install --frozen-lockfile && yarn build +RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build # =================================== # ===== Authelia official image ===== diff --git a/Dockerfile.arm64v8 b/Dockerfile.arm64v8 index 25e1e0853..fb40b2a50 100644 --- a/Dockerfile.arm64v8 +++ b/Dockerfile.arm64v8 @@ -39,7 +39,7 @@ WORKDIR /node/src/app COPY web . # Install the dependencies and build -RUN yarn install --frozen-lockfile && yarn build +RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build # =================================== # ===== Authelia official image ===== diff --git a/Dockerfile.darwin b/Dockerfile.darwin index af39d2b5b..17c3a8bbd 100644 --- a/Dockerfile.darwin +++ b/Dockerfile.darwin @@ -38,7 +38,7 @@ WORKDIR /node/src/app COPY web . # Install the dependencies and build -RUN yarn install --frozen-lockfile && yarn build +RUN yarn install --frozen-lockfile && INLINE_RUNTIME_CHUNK=false yarn build # =================================== # ===== Authelia official image ===== diff --git a/cmd/authelia-scripts/cmd_build.go b/cmd/authelia-scripts/cmd_build.go index 981e48cc4..ee6779fb2 100644 --- a/cmd/authelia-scripts/cmd_build.go +++ b/cmd/authelia-scripts/cmd_build.go @@ -34,6 +34,7 @@ func buildFrontend() { // Then build the frontend cmd = utils.CommandWithStdout("yarn", "build") cmd.Dir = "web" + cmd.Env = append(os.Environ(), "INLINE_RUNTIME_CHUNK=false") if err := cmd.Run(); err != nil { log.Fatal(err) diff --git a/internal/server/index.go b/internal/server/index.go new file mode 100644 index 000000000..4381cf4f9 --- /dev/null +++ b/internal/server/index.go @@ -0,0 +1,46 @@ +package server + +import ( + "fmt" + "html/template" + "io/ioutil" + "os" + + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/internal/logging" + "github.com/authelia/authelia/internal/utils" +) + +var alphaNumericRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + +// ServeIndex serve the index.html file with nonce generated for supporting +// restrictive CSP while using material-ui. +func ServeIndex(publicDir string) fasthttp.RequestHandler { + f, err := os.Open(publicDir + "/index.html") + if err != nil { + logging.Logger().Fatalf("Unable to open index.html: %v", err) + } + + b, err := ioutil.ReadAll(f) + if err != nil { + logging.Logger().Fatalf("Unable to read index.html: %v", err) + } + + tmpl, err := template.New("index").Parse(string(b)) + if err != nil { + logging.Logger().Fatalf("Unable to parse index.html template: %v", err) + } + + return func(ctx *fasthttp.RequestCtx) { + nonce := utils.RandomString(32, alphaNumericRunes) + ctx.SetContentType("text/html; charset=utf-8") + ctx.Response.Header.Add("Content-Security-Policy", fmt.Sprintf("default-src 'self'; style-src 'self' 'nonce-%s'", nonce)) + err := tmpl.Execute(ctx.Response.BodyWriter(), struct{ CSPNonce string }{CSPNonce: nonce}) + if err != nil { + ctx.Error("An error occurred", 503) + logging.Logger().Errorf("Unable to execute template: %v", err) + return + } + } +} \ No newline at end of file diff --git a/internal/server/server.go b/internal/server/server.go index 84a8d1b13..a1a986d81 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,7 +28,8 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi logging.Logger().Infof("Selected public_html directory is %s", publicDir) router := router.New() - router.GET("/", fasthttp.FSHandler(publicDir, 0)) + + router.GET("/", ServeIndex(publicDir)) router.ServeFiles("/static/{filepath:*}", publicDir+"/static") router.GET("/api/state", autheliaMiddleware(handlers.StateGet)) diff --git a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml index 02a473f5b..64a236bee 100644 --- a/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml +++ b/internal/suites/example/compose/authelia/docker-compose.backend.dev.yml @@ -23,6 +23,7 @@ services: - 'traefik.http.services.authelia_backend.loadbalancer.server.scheme=https' environment: - ENVIRONMENT=dev + - PUBLIC_DIR=/tmp/authelia-web networks: authelianet: ipv4_address: 192.168.240.50 diff --git a/internal/suites/example/compose/authelia/resources/entrypoint-backend.sh b/internal/suites/example/compose/authelia/resources/entrypoint-backend.sh index f6290ea38..cd1f88943 100755 --- a/internal/suites/example/compose/authelia/resources/entrypoint-backend.sh +++ b/internal/suites/example/compose/authelia/resources/entrypoint-backend.sh @@ -5,6 +5,11 @@ set -x echo "Use hot reloaded version of Authelia backend" go get github.com/cespare/reflex +# Fake index.html because Authelia reads it as a template at startup to inject nonces. +# This prevents a crash of Authelia in dev mode. +mkdir -p /tmp/authelia-web +touch /tmp/authelia-web/index.html + # Sleep 10 seconds to wait the end of npm install updating web directory # and making reflex reload multiple times. sleep 10 diff --git a/internal/suites/example/compose/authelia/resources/nginx.conf b/internal/suites/example/compose/authelia/resources/nginx.conf index 3961e7a3f..faadeaa72 100644 --- a/internal/suites/example/compose/authelia/resources/nginx.conf +++ b/internal/suites/example/compose/authelia/resources/nginx.conf @@ -13,6 +13,10 @@ http { set $backend_endpoint https://authelia-backend:9091; location / { + # We don't want to apply CSP in dev mode because the frontend is served by CRA + # and thus cannot have the nonce injected. + proxy_hide_header Content-Security-Policy; + proxy_set_header Host $http_host; proxy_pass $backend_endpoint; } diff --git a/web/public/index.html b/web/public/index.html index 489973147..df8a64a79 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -1,6 +1,8 @@ + +