Skip to content

Commit

Permalink
Initial code parts for CSP implementation for sentry and self-hosted (#…
Browse files Browse the repository at this point in the history
…48507)

Another attempt of #47980 (which
was reverted due to bugs).

This PR adds some preliminary code for adding a
`Content-Security-Policy-Report-Only` header with minimal required
permissions (at least I could not find any violations on `sentry
devserver` and self-hosted).
- The CSP middleware is disabled (commented in the `MIDDLEWARE`)
- There is no report collecting enabled by default (`CSP_REPORT_URI` is
not set), the intent is to customize it depending on the use case.
  • Loading branch information
oioki authored May 5, 2023
1 parent 7bc2360 commit 6fbc19f
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 4 deletions.
1 change: 1 addition & 0 deletions requirements-base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ croniter>=1.3.7
cssselect>=1.0.3
datadog>=0.29.3
django-crispy-forms>=1.14.0
django-csp>=3.7
django-pg-zero-downtime-migrations>=0.11
Django>=2.2.28
djangorestframework>=3.12.4
Expand Down
1 change: 1 addition & 0 deletions requirements-dev-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dictpath==0.1.3
distlib==0.3.4
django==2.2.28
django-crispy-forms==1.14.0
django-csp==3.7
django-pg-zero-downtime-migrations==0.11
djangorestframework==3.12.4
docker==3.7.0
Expand Down
1 change: 1 addition & 0 deletions requirements-frozen.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ datadog==0.29.3
decorator==5.1.1
django==2.2.28
django-crispy-forms==1.14.0
django-csp==3.7
django-pg-zero-downtime-migrations==0.11
djangorestframework==3.12.4
drf-spectacular==0.22.1
Expand Down
58 changes: 58 additions & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ def env(
# response modifying middleware reset the Content-Length header.
# This is because CommonMiddleware Sets the Content-Length header for non-streaming responses.
MIDDLEWARE = (
# Uncomment to enable Content Security Policy on this Sentry installation (experimental)
# "csp.middleware.CSPMiddleware",
"sentry.middleware.health.HealthCheck",
"sentry.middleware.security.SecurityHeadersMiddleware",
"sentry.middleware.env.SentryEnvMiddleware",
Expand Down Expand Up @@ -407,6 +409,62 @@ def env(
"urls.E007",
)

CSP_INCLUDE_NONCE_IN = [
"script-src",
]

CSP_DEFAULT_SRC = [
"'none'",
]
CSP_SCRIPT_SRC = [
"'self'",
"'unsafe-inline'",
]
CSP_FONT_SRC = [
"'self'",
"data:",
]
CSP_CONNECT_SRC = [
"'self'",
]
CSP_FRAME_ANCESTORS = [
"'none'",
]
CSP_OBJECT_SRC = [
"'none'",
]
CSP_BASE_URI = [
"'none'",
]
CSP_STYLE_SRC = [
"'self'",
"'unsafe-inline'",
]
CSP_IMG_SRC = [
"'self'",
"blob:",
"data:",
"https://secure.gravatar.com",
]

if ENVIRONMENT == "development":
CSP_SCRIPT_SRC += [
"'unsafe-eval'",
]
CSP_CONNECT_SRC += [
"ws://127.0.0.1:8000",
]

# Before enforcing Content Security Policy, we recommend creating a separate
# Sentry project and collecting CSP violations in report only mode:
# https://docs.sentry.io/product/security-policy-reporting/

# Point this parameter to your Sentry installation:
# CSP_REPORT_URI = "https://example.com/api/{PROJECT_ID}/security/?sentry_key={SENTRY_KEY}"

# To enforce CSP (block violated resources), update the following parameter to False
CSP_REPORT_ONLY = True

STATIC_ROOT = os.path.realpath(os.path.join(PROJECT_ROOT, "static"))
STATIC_URL = "/_static/{version}/"
# webpack assets live at a different URL that is unversioned
Expand Down
31 changes: 27 additions & 4 deletions src/sentry/integrations/jira/views/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from csp.middleware import CSPMiddleware
from django.conf import settings
from django.views.generic import View

from sentry import options
Expand All @@ -15,12 +17,33 @@ def get_response(self, context):
add the requisite CSP headers before returning it.
"""

context["ac_js_src"] = "https://connect-cdn.atl-paas.net/all.js"
response = render_to_response(self.html_file, context, self.request)
sources = [
self.request.GET.get("xdm_e"),
options.get("system.url-prefix"),
]
sources_string = " ".join(s for s in sources if s) # Filter out None
response["Content-Security-Policy"] = f"frame-ancestors 'self' {sources_string}"

settings.CSP_FRAME_ANCESTORS = [
"'self'",
] + [s for s in sources if s and ";" not in s]
settings.CSP_STYLE_SRC = [
# same as default (server.py)
"'self'",
"'unsafe-inline'",
]

if settings.STATIC_FRONTEND_APP_URL.startswith("https://"):
origin = "/".join(settings.STATIC_FRONTEND_APP_URL.split("/")[0:3])
settings.CSP_STYLE_SRC.append(origin)

header = "Content-Security-Policy"
if getattr(settings, "CSP_REPORT_ONLY", False):
header += "-Report-Only"

middleware = CSPMiddleware()
middleware.process_request(self.request) # adds nonce

context["ac_js_src"] = "https://connect-cdn.atl-paas.net/all.js"
response = render_to_response(self.html_file, context, self.request)

response[header] = middleware.build_policy(request=self.request, response=response)
return response
39 changes: 39 additions & 0 deletions tests/sentry/integrations/jira/test_csp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from django.test.utils import override_settings

from sentry.testutils import APITestCase
from sentry.utils.http import absolute_uri


class JiraCSPTest(APITestCase):
def setUp(self):
super().setUp()
self.issue_key = "APP-123"
self.path = absolute_uri(f"extensions/jira/issue/{self.issue_key}/") + "?xdm_e=base_url"

def _split_csp_policy(self, policy):
csp = {}
for directive in policy.split("; "):
parts = directive.split(" ")
csp[parts[0]] = parts[1:]
return csp

def test_csp_frame_ancestors(self):
response = self.client.get(self.path)
assert "Content-Security-Policy-Report-Only" in response

csp = self._split_csp_policy(response["Content-Security-Policy-Report-Only"])
assert "base_url" in csp["frame-ancestors"]
assert "http://testserver" in csp["frame-ancestors"]

@override_settings(STATIC_FRONTEND_APP_URL="https://sentry.io/_static/dist/")
def test_csp_remote_style(self):
response = self.client.get(self.path)
assert "Content-Security-Policy-Report-Only" in response

csp = self._split_csp_policy(response["Content-Security-Policy-Report-Only"])
assert "https://sentry.io" in csp["style-src"]

@override_settings(CSP_REPORT_ONLY=False)
def test_csp_enforce(self):
response = self.client.get(self.path)
assert "Content-Security-Policy" in response

0 comments on commit 6fbc19f

Please sign in to comment.