Repository: getsentry/sentry

Issue:
Handle optional TLS validation for external source fetching
Originally brought up here: https://github.com/getsentry/sentry/pull/4016

We should surface a new setting to require TLS validation when fetching external files. This option would be enabled by default (security first) with an option to disable. We need to make sure we then are able to surface this exact error inside the UI correct so a user can make the decision to disable on their own. To clarify, we shouldn't just blindly say, "Failed to fetch file.", we should strictly say, "Unable to validate TLS certificate when fetching file." or something akin to that.


Discussion:
There's no such thing as a TLS certificate. Maybe the following text would be better:

”Unable to connect via TLS (HTTPS), x509 certificate invalid because:”

Sorry, I'm bad with words. 😛 

It would be nice to have a field for a custom PEM, or support TOFU.

To try and minimise the number of users with validation disabled.

I don't think as a product we should go that far. We support users uploading sources into Sentry, and we'd rather encourage doing that.


Resolving patch:
diff --git a/src/sentry/lang/javascript/processor.py b/src/sentry/lang/javascript/processor.py
--- a/src/sentry/lang/javascript/processor.py
+++ b/src/sentry/lang/javascript/processor.py
@@ -302,7 +302,9 @@ def fetch_file(url, project=None, release=None, allow_scraping=True):
 
     if result is None:
         headers = {}
+        verify_ssl = False
         if project and is_valid_origin(url, project=project):
+            verify_ssl = bool(project.get_option('sentry:verify_ssl', False))
             token = project.get_option('sentry:token')
             if token:
                 token_header = project.get_option(
@@ -312,7 +314,7 @@ def fetch_file(url, project=None, release=None, allow_scraping=True):
                 headers[token_header] = token
 
         with metrics.timer('sourcemaps.fetch'):
-            result = http.fetch_file(url, headers=headers)
+            result = http.fetch_file(url, headers=headers, verify_ssl=verify_ssl)
             z_body = zlib.compress(result.body)
             cache.set(cache_key, (url, result.headers, z_body, result.status, result.encoding), 60)
 
diff --git a/src/sentry/web/frontend/project_settings.py b/src/sentry/web/frontend/project_settings.py
--- a/src/sentry/web/frontend/project_settings.py
+++ b/src/sentry/web/frontend/project_settings.py
@@ -44,6 +44,11 @@ class EditProjectForm(forms.ModelForm):
         }),
         required=False,
     )
+    verify_ssl = forms.BooleanField(
+        label=_('Verify TLS/SSL'),
+        help_text=_('Outbound requests will verify TLS (sometimes known as SSL) connections.'),
+        required=False,
+    )
     resolve_age = RangeField(label=_('Auto resolve'), required=False,
         min_value=0, max_value=168, step_value=1,
         help_text=_('Automatically resolve an issue if it hasn\'t been seen for this amount of time.'))
@@ -243,6 +248,7 @@ def get_form(self, request, project):
                 'origins': '\n'.join(project.get_option('sentry:origins', ['*'])),
                 'token': security_token,
                 'token_header': project.get_option('sentry:token_header'),
+                'verify_ssl': bool(project.get_option('sentry:verify_ssl', False)),
                 'resolve_age': int(project.get_option('sentry:resolve_age', 0)),
                 'scrub_data': bool(project.get_option('sentry:scrub_data', True)),
                 'scrub_defaults': bool(project.get_option('sentry:scrub_defaults', True)),
@@ -265,6 +271,7 @@ def handle(self, request, organization, team, project):
                 'origins',
                 'token',
                 'token_header',
+                'verify_ssl',
                 'resolve_age',
                 'scrub_data',
                 'scrub_defaults',


Test patch: