Skip to content

Commit

Permalink
chore: LEAP-176: set up CSP in Label Studio (#5137)
Browse files Browse the repository at this point in the history
* fix: LEAP-176: set up basic CSP config, sandbox for upload API, report_only=True

* fix user login CSS

* csp checkpoint

* fuller CSP implementation for LS

* remove earlier header setup

* suppress codespell complaint

* ENABLE_LS_CSP -> ENABLE_CSP
  • Loading branch information
jombooth committed Dec 7, 2023
1 parent 36e11d3 commit 1a20250
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 21 deletions.
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
18 changes: 18 additions & 0 deletions label_studio/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import ujson as json
from core.utils.contextlog import ContextLog
from csp.middleware import CSPMiddleware
from django.conf import settings
from django.contrib.auth import logout
from django.core.exceptions import MiddlewareNotUsed
Expand Down Expand Up @@ -207,3 +208,20 @@ def process_request(self, request) -> None:
request.session.set_expiry(
settings.MAX_TIME_BETWEEN_ACTIVITY if request.session.get('keep_me_logged_in', True) else 0
)


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 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']
delattr(response, '_override_report_only_csp')
return response
40 changes: 40 additions & 0 deletions label_studio/core/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,3 +674,43 @@ def collect_versions_dummy(**kwargs):
DATA_MANAGER_FILTER_ALLOWLIST = list(
set(get_env_list('DATA_MANAGER_FILTER_ALLOWLIST') + ['updated_by__active_organization'])
)

if ENABLE_CSP := get_bool_env('ENABLE_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.double' + 'click.net', # hacky way of suppressing codespell complaint
'https://*.ingest.sentry.io',
)
# Note that this will be overridden to real CSP for views that use the override_report_only_csp decorator
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')
8 changes: 6 additions & 2 deletions label_studio/data_import/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +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
from django.conf import settings
from django.db import transaction
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
Expand Down Expand Up @@ -596,6 +598,8 @@ def put(self, *args, **kwargs):
class UploadedFileResponse(generics.RetrieveAPIView):
permission_classes = (IsAuthenticated,)

@override_report_only_csp
@csp(SANDBOX=[])
@swagger_auto_schema(auto_schema=None)
def get(self, *args, **kwargs):
request = self.request
Expand All @@ -613,8 +617,8 @@ def get(self, *args, **kwargs):
content_type, encoding = mimetypes.guess_type(str(file.name))
content_type = content_type or 'application/octet-stream'
return RangedFileResponse(request, file.open(mode='rb'), content_type=content_type)
else:
return Response(status=status.HTTP_404_NOT_FOUND)

return Response(status=status.HTTP_404_NOT_FOUND)


class DownloadStorageData(APIView):
Expand Down
18 changes: 6 additions & 12 deletions label_studio/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<script src="{{settings.HOSTNAME}}{% static 'js/jquery.min.js' %}"></script>
<script src="{{settings.HOSTNAME}}{% static 'js/helpers.js' %}"></script>

<script>
<script nonce="{{request.csp_nonce}}">
EDITOR_JS = "{{settings.HOSTNAME}}/label-studio-frontend/js/main.js?v={{ versions.lsf.commit }}";
EDITOR_CSS = "{{settings.HOSTNAME}}/label-studio-frontend/css/main.css?v={{ versions.lsf.commit }}";
DM_JS = "{{settings.HOSTNAME}}/dm/js/main.js?v={{ versions.dm2.commit }}";
Expand All @@ -37,7 +37,7 @@
integrity="sha384-lowBFC6YTkvMIWPORr7+TERnCkZdo5ab00oH5NkFLeQUAmBTLGwJpFjF6djuxJ/5"
crossorigin="anonymous"></script>

<script>
<script nonce="{{request.csp_nonce}}">
window.exports = () => {};
</script>

Expand All @@ -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>
<main class="main">

<!-- Space & Divider -->
{% block divider %}
Expand All @@ -87,7 +81,7 @@
{% block context_menu_right %}{% endblock %}
</template>

<script id="app-settings">
<script id="app-settings" nonce="{{request.csp_nonce}}">
window.APP_SETTINGS = Object.assign({
user: {
id: {{ user.pk }},
Expand Down Expand Up @@ -124,7 +118,7 @@
<script src="{{settings.HOSTNAME}}/react-app/index.js?v={{ versions.backend.commit }}"></script>

<div id="dynamic-content">
<script>
<script nonce="{{request.csp_nonce}}">
applyCsrf();

$('.message .close').on('click', function () {
Expand All @@ -136,7 +130,7 @@
{% endblock %}

{% block storage-persistence %}
<script>
<script nonce="{{request.csp_nonce}}">
{# storage persistence #}
{% if not settings.STORAGE_PERSISTENCE %}
new Toast({
Expand Down
6 changes: 3 additions & 3 deletions label_studio/users/templates/users/new-ui/user_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
{% block head %}
<link rel="stylesheet" href="{{ settings.HOSTNAME }}{% static 'css/login.css' %}"/>

<script async src="https://www.googletagmanager.com/gtag/js?id=UA-129877673-1"></script>
<script>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-129877673-1" nonce="{{request.csp_nonce}}"></script>
<script nonce="{{request.csp_nonce}}">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
Expand Down Expand Up @@ -42,4 +42,4 @@ <h3>A full-fledged open source solution for data labeling</h3>
</div>

{% endblock %}
{% endblock %}
{% endblock %}
2 changes: 1 addition & 1 deletion label_studio/users/templates/users/new-ui/user_tips.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% load filters %}

<script>
<script nonce="{{request.csp_nonce}}">
const dataTips = [
{
title: 'Did you know?',
Expand Down
4 changes: 2 additions & 2 deletions label_studio/users/templates/users/user_base.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<link rel="stylesheet" href="{{ settings.HOSTNAME }}{% static 'css/login.css' %}"/>

<script async src="https://www.googletagmanager.com/gtag/js?id=UA-129877673-1"></script>
<script>
<script nonce="{{request.csp_nonce}}">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
Expand All @@ -31,4 +31,4 @@ <h2>A full-fledged open source solution for data labeling</h2>

{% block user_content %}
{% endblock %}
{% endblock %}
{% endblock %}
20 changes: 19 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ python-json-logger = "2.0.4"
label-studio-converter = "0.0.57"
google-cloud-storage = "^2.13.0"
mysqlclient = {version = "*", optional = true}
django-csp = "3.7"

[tool.poetry.group.test.dependencies]
pytest = "7.2.2"
Expand Down

0 comments on commit 1a20250

Please sign in to comment.