Skip to content

Commit eb9198e

Browse files
authored
fix: DEV-2235: Fix blind SSRF on add model and import (#2450)
* fix: DEV-2235: Fix blind SSRF on add model and import * Fix ip check (DEV-2235) * Disable bandit check (DEV-2235)
1 parent 12e091e commit eb9198e

File tree

6 files changed

+52
-0
lines changed

6 files changed

+52
-0
lines changed

label_studio/core/settings/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,8 @@
367367
CSRF_COOKIE_SECURE = bool(int(get_env('CSRF_COOKIE_SECURE', SESSION_COOKIE_SECURE)))
368368
CSRF_COOKIE_HTTPONLY = bool(int(get_env('CSRF_COOKIE_HTTPONLY', SESSION_COOKIE_SECURE)))
369369

370+
SSRF_PROTECTION_ENABLED = get_bool_env('SSRF_PROTECTION_ENABLED', False)
371+
370372
# user media files
371373
MEDIA_ROOT = os.path.join(BASE_DATA_DIR, 'media')
372374
os.makedirs(MEDIA_ROOT, exist_ok=True)

label_studio/core/utils/exceptions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,13 @@ class LabelStudioValidationErrorSentryIgnored(ValidationError):
4242

4343
class LabelStudioXMLSyntaxErrorSentryIgnored(Exception):
4444
pass
45+
46+
47+
class ImportFromLocalIPError(LabelStudioAPIException):
48+
default_detail = 'Importing from local IP is not allowed'
49+
status_code = status.HTTP_403_FORBIDDEN
50+
51+
52+
class MLModelLocalIPError(LabelStudioAPIException):
53+
default_detail = 'Adding models with local IP is not allowed'
54+
status_code = status.HTTP_403_FORBIDDEN

label_studio/core/utils/io.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
22
"""
33
import os
4+
import socket
5+
import ipaddress
46
import pkg_resources
57
import shutil
68
import glob
@@ -9,6 +11,7 @@
911
import itertools
1012
import yaml
1113

14+
from urllib.parse import urlparse
1215
from contextlib import contextmanager
1316
from tempfile import mkstemp, mkdtemp
1417

@@ -163,3 +166,27 @@ def __init__(self, iterable):
163166

164167
def __iter__(self):
165168
return itertools.chain(self._head, *self[:1])
169+
170+
171+
def url_is_local(url):
172+
domain = urlparse(url).hostname
173+
try:
174+
ip = socket.gethostbyname(domain)
175+
except socket.error:
176+
from core.utils.exceptions import LabelStudioAPIException
177+
raise LabelStudioAPIException(f"Can't resolve hostname {domain}")
178+
else:
179+
if ip in (
180+
'0.0.0.0', # nosec
181+
):
182+
return True
183+
local_subnets = [
184+
'127.0.0.0/8',
185+
'10.0.0.0/8',
186+
'172.16.0.0/12',
187+
'192.168.0.0/16',
188+
]
189+
for subnet in local_subnets:
190+
if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet):
191+
return True
192+
return False

label_studio/data_import/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ def create(self, request, *args, **kwargs):
205205
project = generics.get_object_or_404(Project.objects.for_user(self.request.user), pk=self.kwargs['pk'])
206206

207207
# upload files from request, and parse all tasks
208+
# TODO: Stop passing request to load_tasks function, make all validation before
208209
parsed_data, file_upload_ids, could_be_tasks_lists, found_formats, data_columns = load_tasks(request, project)
209210

210211
if preannotated_from_fields:

label_studio/data_import/uploader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from urllib.request import urlopen
2020

2121
from .models import FileUpload
22+
from core.utils.io import url_is_local
23+
from core.utils.exceptions import ImportFromLocalIPError
2224

2325
logger = logging.getLogger(__name__)
2426
csv.field_size_limit(131072 * 10)
@@ -131,6 +133,8 @@ def load_tasks(request, project):
131133

132134
# download file using url and read tasks from it
133135
else:
136+
if settings.SSRF_PROTECTION_ENABLED and url_is_local(url):
137+
raise ImportFromLocalIPError
134138
data_keys, found_formats, tasks, file_upload_ids = tasks_from_url(
135139
file_upload_ids, project, request, url
136140
)

label_studio/ml/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
"""This file and its contents are licensed under the Apache License 2.0. Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
22
"""
3+
from django.conf import settings
34
from rest_framework import serializers
45
from ml.models import MLBackend
6+
from core.utils.io import url_is_local
7+
from core.utils.exceptions import MLModelLocalIPError
58

69

710
class MLBackendSerializer(serializers.ModelSerializer):
11+
def validate_url(self, value):
12+
if settings.SSRF_PROTECTION_ENABLED and url_is_local(value):
13+
raise MLModelLocalIPError
14+
return value
15+
816
def validate(self, attrs):
917
attrs = super(MLBackendSerializer, self).validate(attrs)
1018
url = attrs['url']

0 commit comments

Comments
 (0)