diff --git a/src/worker-auth.c b/src/worker-auth.c index 4e9e1459..761c8813 100644 --- a/src/worker-auth.c +++ b/src/worker-auth.c @@ -438,6 +438,12 @@ int get_auth_handler2(worker_st * ws, unsigned http_ver, const char *pmsg, unsig goto cleanup; } + ret = add_owasp_headers(ws); + if (ret < 0) { + ret = -1; + goto cleanup; + } + ret = cstp_puts(ws, "\r\n"); if (ret < 0) { ret = -1; @@ -1089,6 +1095,12 @@ int post_common_handler(worker_st * ws, unsigned http_ver, const char *imsg) if (ret < 0) goto fail; + ret = + add_owasp_headers(ws); + if (ret < 0) + goto fail; + + #ifdef ANYCONNECT_CLIENT_COMPAT if (WSCONFIG(ws)->xml_config_file) { ret = diff --git a/src/worker-http-handlers.c b/src/worker-http-handlers.c index 0d405798..4df5c2cd 100644 --- a/src/worker-http-handlers.c +++ b/src/worker-http-handlers.c @@ -58,6 +58,7 @@ static int send_headers(worker_st *ws, unsigned http_ver, const char *content_ty cstp_printf(ws, "Content-Type: %s\r\n", content_type) < 0 || cstp_puts (ws, "X-Transcend-Version: 1\r\n") < 0 || cstp_printf(ws, "Content-Length: %u\r\n", content_length) < 0 || + add_owasp_headers(ws) < 0 || cstp_puts (ws, "\r\n") < 0) return -1; return 0; diff --git a/src/worker-http.c b/src/worker-http.c index 7ca584ef..adf00d5b 100644 --- a/src/worker-http.c +++ b/src/worker-http.c @@ -863,3 +863,28 @@ void http_req_deinit(worker_st * ws) ws->req.body = NULL; } +/* add_owasp_headers: + * @ws: an initialized worker structure + * + * This function adds the OWASP default headers + * There are security tools that flag the server as a security risk. + * These are added to help users comply with security best practices. + */ +int add_owasp_headers(worker_st * ws) +{ + if (cstp_puts(ws, "Strict-Transport-Security: max-age=31536000 ; includeSubDomains\r\n") < 0 || + cstp_puts(ws, "X-Frame-Options: deny\r\n") < 0 || + cstp_puts(ws, "X-Content-Type-Options: nosniff\r\n") < 0 || + cstp_puts(ws, "Content-Security-Policy: default-src \'none\'\r\n") < 0 || + cstp_puts(ws, "X-Permitted-Cross-Domain-Policies: none\r\n") < 0 || + cstp_puts(ws, "Referrer-Policy: no-referrer\r\n") < 0 || + cstp_puts(ws, "Clear-Site-Data: \"cache\",\"cookies\",\"storage\"\r\n") < 0 || + cstp_puts(ws, "Cross-Origin-Embedder-Policy: require-corp\r\n") < 0 || + cstp_puts(ws, "Cross-Origin-Opener-Policy: same-origin\r\n") < 0 || + cstp_puts(ws, "Cross-Origin-Resource-Policy: same-origin\r\n") < 0 || + cstp_puts(ws, "X-XSS-Protection: 0\r\n") < 0) + { + return -1; + } + return 0; +} \ No newline at end of file diff --git a/src/worker-kkdcp.c b/src/worker-kkdcp.c index 817fe2f8..2e126536 100644 --- a/src/worker-kkdcp.c +++ b/src/worker-kkdcp.c @@ -273,6 +273,11 @@ int post_kkdcp_handler(worker_st *ws, unsigned http_ver) goto fail; } + ret = add_owasp_headers(ws); + if (ret < 0) { + goto fail; + } + ret = cstp_puts(ws, "\r\n"); if (ret < 0) { goto fail; diff --git a/src/worker-vpn.c b/src/worker-vpn.c index e747768f..228174a5 100644 --- a/src/worker-vpn.c +++ b/src/worker-vpn.c @@ -1931,6 +1931,9 @@ static int connect_handler(worker_st * ws) ret = cstp_puts(ws, "HTTP/1.1 200 CONNECTED\r\n"); SEND_ERR(ret); + ret = add_owasp_headers(ws); + SEND_ERR(ret); + ret = cstp_puts(ws, "X-CSTP-Version: 1\r\n"); SEND_ERR(ret); diff --git a/src/worker.h b/src/worker.h index cf9443ff..be94e415 100644 --- a/src/worker.h +++ b/src/worker.h @@ -422,6 +422,8 @@ int parse_proxy_proto_header(struct worker_st *ws, int fd); void cookie_authenticate_or_exit(worker_st *ws); +int add_owasp_headers(worker_st * ws); + /* after that time (secs) of inactivity in the UDP part, connection switches to * TCP (if activity occurs there). */ diff --git a/tests/Makefile.am b/tests/Makefile.am index a419e2ad..8e86d6f1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -180,6 +180,7 @@ check_PROGRAMS += gen_oidc_test_data dist_check_SCRIPTS += test-oidc endif +dist_check_SCRIPTS += test-owasp-headers dist_check_SCRIPTS += test-replay diff --git a/tests/test-owasp-headers b/tests/test-owasp-headers new file mode 100755 index 00000000..870fece4 --- /dev/null +++ b/tests/test-owasp-headers @@ -0,0 +1,115 @@ +#!/bin/bash +# +# Copyright (C) 2021 Microsoft Corporation +# +# This file is part of ocserv. +# +# ocserv is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# ocserv is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GnuTLS; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +NO_NEED_ROOT=1 + +. `dirname $0`/common.sh + +eval "${GETPORT}" + +echo "Testing ocserv owasp headers... " + +update_config test-user-cert.config +launch_simple_sr_server -d 1 -f -c ${CONFIG} +PID=$! + +wait_server $PID + +function CheckHeaders +{ + [[ "$1" =~ .*"Strict-Transport-Security".* ]] || fail $PID "Missing HTTP header (Strict-Transport-Security)" + [[ "$1" =~ .*"X-Frame-Options".* ]] || fail $PID "Missing HTTP header (X-Frame-Options)" + [[ "$1" =~ .*"X-Content-Type-Options".* ]] || fail $PID "Missing HTTP header (X-Content-Type-Options)" + [[ "$1" =~ .*"Content-Security-Policy".* ]] || fail $PID "Missing HTTP header (Content-Security-Policy)" + [[ "$1" =~ .*"X-Permitted-Cross-Domain-Policies".* ]] || fail $PID "Missing HTTP header (X-Permitted-Cross-Domain-Policies)" + [[ "$1" =~ .*"Referrer-Policy".* ]] || fail $PID "Missing HTTP header (Referrer-Policy)" + [[ "$1" =~ .*"Clear-Site-Data".* ]] || fail $PID "Missing HTTP header (Clear-Site-Data)" + [[ "$1" =~ .*"Cross-Origin-Embedder-Policy".* ]] || fail $PID "Missing HTTP header (Cross-Origin-Embedder-Policy)" + [[ "$1" =~ .*"Cross-Origin-Opener-Policy".* ]] || fail $PID "Missing HTTP header (Cross-Origin-Opener-Policy)" + [[ "$1" =~ .*"Cross-Origin-Resource-Policy".* ]] || fail $PID "Missing HTTP header (Cross-Origin-Resource-Policy)" + [[ "$1" =~ .*"X-XSS-Protection".* ]] || fail $PID "Missing HTTP header (X-XSS-Protection)" + + while IFS=':' read name value; do + case "$name" in + Strict-Transport-Security) + [[ "$value" =~ "max-age=31536000 ; includeSubDomains" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + X-Frame-Options) + [[ "$value" =~ "deny" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + X-Content-Type-Options) + [[ "$value" =~ "nosniff" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Content-Security-Policy) + [[ "$value" =~ "default-src 'none'" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + X-Permitted-Cross-Domain-Policies) + [[ "$value" =~ "none" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Referrer-Policy) + [[ "$value" =~ "no-referrer" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Clear-Site-Data) + [[ "$value" =~ "\"cache\",\"cookies\",\"storage\"" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Cross-Origin-Embedder-Policy) + [[ "$value" =~ "require-corp" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Cross-Origin-Opener-Policy) + [[ "$value" =~ "same-origin" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + Cross-Origin-Resource-Policy) + [[ "$value" =~ "same-origin" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + X-XSS-Protection) + [[ "$value" =~ "0" ]] || fail $PID "Unexpected HTTP header value ($name: $value)";; + esac + done < <(echo "$1") +} + +echo -n "Testing / ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/ --insecure) +CheckHeaders "$results" + +echo -n "Testing /auth ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/auth --insecure) +CheckHeaders "$results" + +echo -n "Testing /VPN ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/VPN --insecure) +CheckHeaders "$results" +echo "ok" + +echo -n "Testing /cert.pem ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/cert.pem --insecure) +CheckHeaders "$results" +echo "ok" + +echo -n "Testing /cert.cer ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/cert.cer --insecure) +CheckHeaders "$results" +echo "ok" + +echo -n "Testing /ca.pem ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/ca.pem --insecure) +CheckHeaders "$results" +echo "ok" + +echo -n "Testing /ca.cer ... " +results=$(LD_PRELOAD=libsocket_wrapper.so curl -I -X GET -s https://$ADDRESS:$PORT/ca.cer --insecure) +CheckHeaders "$results" +echo "ok" + + +cleanup + +exit 0