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
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_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.double' + 'click.net', # hacky way of suppressing codespell complaint
jombooth marked this conversation as resolved.
Show resolved Hide resolved
'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')
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>
jombooth marked this conversation as resolved.
Show resolved Hide resolved
<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