Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: LEAP-176: set up CSP in Label Studio #5137

Merged
merged 10 commits into from
Dec 7, 2023
Prev Previous commit
Next Next commit
fuller CSP implementation for LS
  • Loading branch information
jombooth committed Dec 4, 2023
commit 6cc253430e41fbf9fe84a8f7f7257215a960c705
17 changes: 17 additions & 0 deletions label_studio/core/decorators.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from functools import wraps


def permission_required(*permissions, fn=None):
def decorator(view):
def wrapped_view(self, request, *args, **kwargs):
Expand All @@ -19,3 +22,17 @@ def wrapped_view(self, request, *args, **kwargs):
return wrapped_view

return decorator


def override_report_only_csp(view_func):
"""
Decorator to switch report-only CSP to regular CSP. For use with core.middleware.HumanSignalCspMiddleware.
"""

@wraps(view_func)
def wrapper(*args, **kwargs):
response = view_func(*args, **kwargs)
setattr(response, '_override_report_only_csp', True)
return response

return wrapper
14 changes: 10 additions & 4 deletions label_studio/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,18 @@ def process_request(self, request) -> None:
)


class HumanSignalCSPMiddleware(CSPMiddleware):
class HumanSignalCspMiddleware(CSPMiddleware):
"""
Extend CSPMiddleware to support switching report-only CSP to regular CSP.

For use with core.decorators.override_report_only_csp.
"""

def process_response(self, request, response):
response = super().process_response(request, response)
if csp_policy := response.get('Content-Security-Policy-Report-Only'):
if response.get('override-report-only-csp'):
if getattr(response, '_override_report_only_csp', False):
if csp_policy := response.get('Content-Security-Policy-Report-Only'):
response['Content-Security-Policy'] = csp_policy
del response['Content-Security-Policy-Report-Only']
del response['override-report-only-csp']
delattr(response, '_override_report_only_csp')
return response
66 changes: 39 additions & 27 deletions label_studio/core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@
'core.middleware.ContextLogMiddleware',
'core.middleware.DatabaseIsLockedRetryMiddleware',
'core.current_request.ThreadLocalMiddleware',
'core.middleware.HumanSignalCSPMiddleware',
]

REST_FRAMEWORK = {
Expand Down Expand Up @@ -676,29 +675,42 @@ def collect_versions_dummy(**kwargs):
set(get_env_list('DATA_MANAGER_FILTER_ALLOWLIST') + ['updated_by__active_organization'])
)

CSP_DEFAULT_SRC = ("'self'",)
CSP_STYLE_SRC = ("'self'",)
CSP_SCRIPT_SRC = (
"'self'",
'blob:',
'browser.sentry-cdn.com',
'https://*.googletagmanager.com',
)
CSP_IMG_SRC = (
"'self'",
'data:',
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://*.google.com',
)
CSP_CONNECT_SRC = (
"'self'",
'https://*.google-analytics.com',
'https://*.analytics.google.com',
'https://analytics.google.com',
'https://*.googletagmanager.com',
'https://*.g.doubleclick.net',
'https://*.ingest.sentry.io',
)
CSP_REPORT_ONLY = True
CSP_INCLUDE_NONCE_IN = ['script-src', 'default-src', 'style-src']
if ENABLE_LS_CSP := get_bool_env('ENABLE_LS_CSP', True):
CSP_DEFAULT_SRC = (
"'self'",
"'report-sample'",
)
CSP_STYLE_SRC = ("'self'", "'report-sample'", "'unsafe-inline'")
CSP_SCRIPT_SRC = (
"'self'",
"'report-sample'",
"'unsafe-inline'",
"'unsafe-eval'",
'blob:',
'browser.sentry-cdn.com',
'https://*.googletagmanager.com',
)
CSP_IMG_SRC = (
"'self'",
"'report-sample'",
'data:',
'https://*.google-analytics.com',
'https://*.googletagmanager.com',
'https://*.google.com',
)
CSP_CONNECT_SRC = (
"'self'",
"'report-sample'",
'https://*.google-analytics.com',
'https://*.analytics.google.com',
'https://analytics.google.com',
'https://*.googletagmanager.com',
'https://*.g.doubleclick.net',
'https://*.ingest.sentry.io',
)
# Note that this will be overridden to true CSP for views that use the override_report_only_csp decorator
jombooth marked this conversation as resolved.
Show resolved Hide resolved
CSP_REPORT_ONLY = get_bool_env('LS_CSP_REPORT_ONLY', True)
CSP_REPORT_URI = get_env('LS_CSP_REPORT_URI', None)
CSP_INCLUDE_NONCE_IN = ['script-src', 'default-src']

MIDDLEWARE.append('core.middleware.HumanSignalCspMiddleware')
6 changes: 4 additions & 2 deletions label_studio/data_import/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from urllib.parse import unquote, urlparse

import drf_yasg.openapi as openapi
from core.decorators import override_report_only_csp
from core.feature_flags import flag_set
from core.permissions import ViewClassPermission, all_permissions
from core.redis import start_job_async_or_sync
from core.utils.common import retry_database_locked, timeit
from core.utils.exceptions import LabelStudioValidationErrorSentryIgnored
from core.utils.params import bool_from_request, list_of_strings_from_request
from csp.decorators import csp_update
from csp.decorators import csp
from django.conf import settings
from django.db import transaction
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
Expand Down Expand Up @@ -597,7 +598,8 @@ def put(self, *args, **kwargs):
class UploadedFileResponse(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)

@csp_update(SANDBOX=[])
@override_report_only_csp
@csp(SANDBOX=[])
@swagger_auto_schema(auto_schema=None)
def get(self, *args, **kwargs):
request = self.request
Expand Down
8 changes: 1 addition & 7 deletions label_studio/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,7 @@
<div class="app-wrapper"></div>

<template id="main-content">
<main class="main" style="background: transparent">

<div class="ui floating dropdown theme basic" style="float: right;">
{% block top-buttons %}
{% endblock %}
</div>
</div>
jombooth marked this conversation as resolved.
Show resolved Hide resolved
<main class="main">

<!-- Space & Divider -->
{% block divider %}
Expand Down
7 changes: 1 addition & 6 deletions label_studio/users/templates/users/new-ui/user_login.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@
{% endfor %}
{% endif %}
<p>
<input type="checkbox" id="persist_session" name="persist_session" class="ls-checkbox autowidth" checked="checked" />
<style nonce="{{request.csp_nonce}}" >
.autowidth {
width: auto !important;
}
</style>
<input type="checkbox" id="persist_session" name="persist_session" class="ls-checkbox" checked="checked" style="width: auto;" />
<label for="persist_session">Keep me logged in this browser</label>
</p>
<p><button type="submit" aria-label="Log In" class="ls-button ls-button_look_primary">Log in</button></p>
Expand Down
7 changes: 1 addition & 6 deletions label_studio/users/templates/users/user_login.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,8 @@
{% endfor %}
{% endif %}
<p>
<input type="checkbox" id="persist_session" name="persist_session" class="ls-checkbox autowidth" checked="checked" />
<input type="checkbox" id="persist_session" name="persist_session" class="ls-checkbox" checked="checked" style="width: auto;" />
<label for="persist_session">Keep me logged in this browser</label>
<style nonce="{{request.csp_nonce}}" >
.autowidth {
width: auto !important;
}
</style>
</p>
<p><button type="submit" aria-label="Log In" class="ls-button ls-button_look_primary">Log in</button></p>
</form>
Expand Down