From 0b8f3887c06a671d9e8d58eda397dad68ac6b38f Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Wed, 28 Jan 2026 02:13:32 +0000 Subject: [PATCH] Tests and test environment --- .devcontainer/scripts/setup.sh | 6 +- .../test_nginx_proxy_security.py | 136 ++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 test/api_endpoints/test_nginx_proxy_security.py diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index f766bd0e..2a9df319 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -32,7 +32,6 @@ LOG_FILES=( LOG_DB_IS_LOCKED LOG_NGINX_ERROR ) - sudo chmod 666 /var/run/docker.sock 2>/dev/null || true sudo chown "$(id -u)":"$(id -g)" /workspaces sudo chmod 755 /workspaces @@ -55,6 +54,9 @@ sudo install -d -m 777 /tmp/log/plugins sudo rm -rf /entrypoint.d sudo ln -s "${SOURCE_DIR}/install/production-filesystem/entrypoint.d" /entrypoint.d +sudo rm -rf /services +sudo ln -s "${SOURCE_DIR}/install/production-filesystem/services" /services + sudo rm -rf "${NETALERTX_APP}" sudo ln -s "${SOURCE_DIR}/" "${NETALERTX_APP}" @@ -88,8 +90,6 @@ sudo chmod 777 "${LOG_DB_IS_LOCKED}" sudo pkill -f python3 2>/dev/null || true -sudo chmod -R 777 "${PY_SITE_PACKAGES}" "${NETALERTX_DATA}" 2>/dev/null || true - sudo chown -R "${NETALERTX_USER}:${NETALERTX_GROUP}" "${NETALERTX_APP}" date +%s | sudo tee "${NETALERTX_FRONT}/buildtimestamp.txt" >/dev/null diff --git a/test/api_endpoints/test_nginx_proxy_security.py b/test/api_endpoints/test_nginx_proxy_security.py new file mode 100644 index 00000000..26849606 --- /dev/null +++ b/test/api_endpoints/test_nginx_proxy_security.py @@ -0,0 +1,136 @@ +import pytest +import requests +import os + +# Nginx listens on PORT, default 20211 +PORT = os.environ.get("PORT", "20211") +BACKEND_PORT = os.environ.get("BACKEND_PORT", "20212") +BASE_URL = f"http://localhost:{PORT}/server/" + + +def test_nginx_proxy_security_modern_check(): + """ + Test that access is allowed when Sec-Fetch-Site is 'same-origin'. + """ + headers = { + "Sec-Fetch-Site": "same-origin" + } + try: + response = requests.get(BASE_URL, headers=headers) + # 200 (OK), 401 (Auth), 404 (Not Found on backend), or 502 (Bad Gateway) means Nginx let it through. + # 403 means Nginx blocked it. + assert response.status_code in [200, 401, 404, 502], f"Expected access allowed, got {response.status_code}" + except requests.exceptions.ConnectionError: + pytest.fail("Could not connect to Nginx. Is it running?") + + +def test_nginx_proxy_security_legacy_check(): + """ + Test that access is allowed when Sec-Fetch-Site is missing but Referer matches host. + This is for old tablets/phones which are not updated in the last few years. + """ + headers = { + # No Sec-Fetch-Site + "Referer": f"http://localhost:{PORT}/some/page" + } + try: + response = requests.get(BASE_URL, headers=headers) + assert response.status_code in [200, 401, 404, 502], f"Expected access allowed, got {response.status_code}" + except requests.exceptions.ConnectionError: + pytest.fail("Could not connect to Nginx. Is it running?") + + +def test_nginx_proxy_security_block_cross_site(): + """ + Test that access is BLOCKED when Sec-Fetch-Site is 'cross-site'. + """ + headers = { + "Sec-Fetch-Site": "cross-site" + } + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403, f"Expected 403 Forbidden, got {response.status_code}" + + +def test_nginx_proxy_security_block_no_headers(): + """ + Test that access is BLOCKED when no security headers are present. + """ + headers = {} + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403, f"Expected 403 Forbidden, got {response.status_code}" + + +def test_nginx_proxy_security_block_same_site(): + """ + Test that access is BLOCKED when Sec-Fetch-Site is 'same-site'. + (Strict same-origin enforcement) + """ + headers = {"Sec-Fetch-Site": "same-site"} + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403, f"Expected 403 for same-site, got {response.status_code}" + + +def test_nginx_proxy_security_block_referer_suffix_spoof(): + """ + Test that access is BLOCKED when Referer merely ends with the valid host. + """ + headers = {"Referer": f"http://attacker.com/path?target=localhost:{PORT}"} + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403 + + +def test_nginx_proxy_security_block_bad_referer(): + """ + Test that access is BLOCKED when Sec-Fetch-Site is missing and Referer is external. + """ + headers = { + "Referer": "http://evil.com/page" + } + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403, f"Expected 403 Forbidden, got {response.status_code}" + + +def test_nginx_proxy_security_block_subdomain_referer(): + """ + Test that access is BLOCKED when Referer is a subdomain (same-site, not same-origin). + """ + headers = { + "Referer": f"http://subdomain.localhost:{PORT}/" + } + response = requests.get(BASE_URL, headers=headers) + assert response.status_code == 403, f"Expected 403 for subdomain referer, got {response.status_code}" + + +def test_nginx_proxy_security_legacy_protocol_agnostic(): + """ + Test that the legacy check allows both http and https referers. + """ + headers = {"Referer": f"https://localhost:{PORT}/path"} + response = requests.get(BASE_URL, headers=headers) + assert response.status_code in [200, 401, 404, 502] + + +def test_nginx_proxy_security_block_server_docs(): + """ + Test that access to `/server/docs` is BLOCKED when navigating with browser (no referrer) + """ + url = f"http://localhost:{PORT}/server/docs" + try: + response = requests.get(url) + # Backend may return 404 if it doesn't have the path; Nginx should never allow a 200 here. + assert response.status_code == 403, f"Expected 403 for /server/docs, got {response.status_code}" + except requests.exceptions.ConnectionError: + pytest.fail("Could not connect to Nginx. Is it running?") + + +def test_nginx_proxy_security_allow_port(): + """ + Test that access to `:20212/docs` is allowed by Nginx (should return 200). + """ + headers = {"Referer": f"https://localhost:{BACKEND_PORT}/path"} + url = f"http://localhost:{BACKEND_PORT}/docs" + try: + response = requests.get(url, headers=headers) + assert response.status_code == 200, f"Expected 200 for /server/docs on allowed port, got {response.status_code}" + except requests.exceptions.ConnectionError: + pytest.fail("Could not connect to Nginx. Is it running?")