mirror of
https://github.com/frappe/frappe_docker.git
synced 2026-06-17 13:55:08 +00:00
Merge pull request #1838 from RinZ27/fix-nginx-header-inheritance
Ensure security headers are preserved in /files location
This commit is contained in:
commit
3ce80020b7
6 changed files with 64 additions and 21 deletions
|
|
@ -63,6 +63,7 @@ RUN useradd -ms /bin/bash frappe \
|
||||||
# Clean up
|
# Clean up
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& rm -fr /etc/nginx/sites-enabled/default \
|
&& rm -fr /etc/nginx/sites-enabled/default \
|
||||||
|
&& mkdir -p /etc/nginx/snippets \
|
||||||
&& pip3 install frappe-bench \
|
&& pip3 install frappe-bench \
|
||||||
# Fixes for non-root nginx and logs to stdout
|
# Fixes for non-root nginx and logs to stdout
|
||||||
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
|
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
|
||||||
|
|
@ -70,12 +71,15 @@ RUN useradd -ms /bin/bash frappe \
|
||||||
&& touch /run/nginx.pid \
|
&& touch /run/nginx.pid \
|
||||||
&& chown -R frappe:frappe /etc/nginx/conf.d \
|
&& chown -R frappe:frappe /etc/nginx/conf.d \
|
||||||
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
|
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
|
||||||
|
&& chown -R frappe:frappe /etc/nginx/snippets \
|
||||||
&& chown -R frappe:frappe /var/log/nginx \
|
&& chown -R frappe:frappe /var/log/nginx \
|
||||||
&& chown -R frappe:frappe /var/lib/nginx \
|
&& chown -R frappe:frappe /var/lib/nginx \
|
||||||
&& chown -R frappe:frappe /run/nginx.pid \
|
&& chown -R frappe:frappe /run/nginx.pid \
|
||||||
&& chmod 755 /usr/local/bin/nginx-entrypoint.sh \
|
&& chmod 755 /usr/local/bin/nginx-entrypoint.sh \
|
||||||
&& chmod 644 /templates/nginx/frappe.conf.template
|
&& chmod 644 /templates/nginx/frappe.conf.template
|
||||||
|
|
||||||
|
COPY resources/core/nginx/security_headers.conf /etc/nginx/snippets/security_headers.conf
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ RUN useradd -ms /bin/bash frappe \
|
||||||
# Clean up
|
# Clean up
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& rm -fr /etc/nginx/sites-enabled/default \
|
&& rm -fr /etc/nginx/sites-enabled/default \
|
||||||
|
&& mkdir -p /etc/nginx/snippets \
|
||||||
&& pip3 install frappe-bench \
|
&& pip3 install frappe-bench \
|
||||||
# Fixes for non-root nginx and logs to stdout
|
# Fixes for non-root nginx and logs to stdout
|
||||||
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
|
&& sed -i '/user www-data/d' /etc/nginx/nginx.conf \
|
||||||
|
|
@ -67,12 +68,14 @@ RUN useradd -ms /bin/bash frappe \
|
||||||
&& touch /run/nginx.pid \
|
&& touch /run/nginx.pid \
|
||||||
&& chown -R frappe:frappe /etc/nginx/conf.d \
|
&& chown -R frappe:frappe /etc/nginx/conf.d \
|
||||||
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
|
&& chown -R frappe:frappe /etc/nginx/nginx.conf \
|
||||||
|
&& chown -R frappe:frappe /etc/nginx/snippets \
|
||||||
&& chown -R frappe:frappe /var/log/nginx \
|
&& chown -R frappe:frappe /var/log/nginx \
|
||||||
&& chown -R frappe:frappe /var/lib/nginx \
|
&& chown -R frappe:frappe /var/lib/nginx \
|
||||||
&& chown -R frappe:frappe /run/nginx.pid
|
&& chown -R frappe:frappe /run/nginx.pid
|
||||||
|
|
||||||
COPY resources/core/nginx/nginx-template.conf /templates/nginx/frappe.conf.template
|
COPY resources/core/nginx/nginx-template.conf /templates/nginx/frappe.conf.template
|
||||||
COPY resources/core/nginx/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh
|
COPY resources/core/nginx/nginx-entrypoint.sh /usr/local/bin/nginx-entrypoint.sh
|
||||||
|
COPY resources/core/nginx/security_headers.conf /etc/nginx/snippets/security_headers.conf
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,7 @@ server {
|
||||||
proxy_buffers 4 256k;
|
proxy_buffers 4 256k;
|
||||||
proxy_busy_buffers_size 256k;
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN";
|
include /etc/nginx/snippets/security_headers.conf;
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin";
|
|
||||||
|
|
||||||
set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS};
|
set_real_ip_from ${UPSTREAM_REAL_IP_ADDRESS};
|
||||||
real_ip_header ${UPSTREAM_REAL_IP_HEADER};
|
real_ip_header ${UPSTREAM_REAL_IP_HEADER};
|
||||||
|
|
@ -59,6 +55,7 @@ server {
|
||||||
rewrite ^(.+)\.html$ $1 permanent;
|
rewrite ^(.+)\.html$ $1 permanent;
|
||||||
|
|
||||||
location ~ ^/files/.*.(htm|html|svg|xml) {
|
location ~ ^/files/.*.(htm|html|svg|xml) {
|
||||||
|
include /etc/nginx/snippets/security_headers.conf;
|
||||||
add_header Content-disposition "attachment";
|
add_header Content-disposition "attachment";
|
||||||
try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver;
|
try_files /${FRAPPE_SITE_NAME_HEADER}/public/$uri @webserver;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
resources/core/nginx/security_headers.conf
Normal file
5
resources/core/nginx/security_headers.conf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin" always;
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import Any
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.conftest import S3ServiceResult
|
from tests.conftest import S3ServiceResult
|
||||||
from tests.utils import Compose, check_url_content
|
from tests.utils import Compose, check_url_content, wait_for_url
|
||||||
|
|
||||||
BACKEND_SERVICES = (
|
BACKEND_SERVICES = (
|
||||||
"backend",
|
"backend",
|
||||||
|
|
@ -81,6 +81,30 @@ def test_files_reachable(frappe_site: str, tmp_path: Path, compose: Compose):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_files_html_security_headers(
|
||||||
|
frappe_site: str, tmp_path: Path, compose: Compose
|
||||||
|
):
|
||||||
|
file_path = tmp_path / "testfile.html"
|
||||||
|
file_path.write_text(
|
||||||
|
"<html><body>This is a Frappe Docker test html file</body></html>"
|
||||||
|
)
|
||||||
|
|
||||||
|
compose(
|
||||||
|
"cp",
|
||||||
|
str(file_path),
|
||||||
|
f"backend:/home/frappe/frappe-bench/sites/{frappe_site}/public/files/",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = wait_for_url(
|
||||||
|
url=f"http://127.0.0.1/files/{file_path.name}",
|
||||||
|
site_name=frappe_site,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.headers["Content-Disposition"] == "attachment"
|
||||||
|
assert response.headers["X-Frame-Options"] == "SAMEORIGIN"
|
||||||
|
assert response.headers["X-Content-Type-Options"] == "nosniff"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("service", BACKEND_SERVICES)
|
@pytest.mark.parametrize("service", BACKEND_SERVICES)
|
||||||
@pytest.mark.usefixtures("frappe_site")
|
@pytest.mark.usefixtures("frappe_site")
|
||||||
def test_frappe_connections_in_backends(
|
def test_frappe_connections_in_backends(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
|
from http.client import HTTPResponse
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
@ -59,24 +60,11 @@ class Compose:
|
||||||
def check_url_content(
|
def check_url_content(
|
||||||
url: str, callback: Callable[[str], Optional[str]], site_name: str
|
url: str, callback: Callable[[str], Optional[str]], site_name: str
|
||||||
):
|
):
|
||||||
request = Request(url, headers={"Host": site_name})
|
|
||||||
|
|
||||||
# This is needed to check https override
|
|
||||||
ctx = ssl.create_default_context()
|
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
for _ in range(100):
|
for _ in range(100):
|
||||||
try:
|
try:
|
||||||
response = urlopen(request, context=ctx)
|
response = wait_for_url(url=url, site_name=site_name, attempts=1)
|
||||||
|
except RuntimeError:
|
||||||
except HTTPError as exc:
|
|
||||||
if exc.code not in (404, 502):
|
|
||||||
raise
|
|
||||||
|
|
||||||
except URLError:
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
else:
|
else:
|
||||||
text: str = response.read().decode()
|
text: str = response.read().decode()
|
||||||
ret = callback(text)
|
ret = callback(text)
|
||||||
|
|
@ -86,4 +74,26 @@ def check_url_content(
|
||||||
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
raise RuntimeError(f"Couldn't verify expected content from {url}")
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_url(url: str, site_name: str, attempts: int = 100) -> HTTPResponse:
|
||||||
|
request = Request(url, headers={"Host": site_name})
|
||||||
|
|
||||||
|
# This is needed to check https override
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
for _ in range(attempts):
|
||||||
|
try:
|
||||||
|
return urlopen(request, context=ctx)
|
||||||
|
except HTTPError as exc:
|
||||||
|
if exc.code not in (404, 502):
|
||||||
|
raise
|
||||||
|
except URLError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
raise RuntimeError(f"Couldn't ping {url}")
|
raise RuntimeError(f"Couldn't ping {url}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue