Skip to content

Commit 7dfbdbe

Browse files
committed
Remove exception tracebacks from failed tasks
Tracebacks can expose sensitive information from an exception via the API. This change stops this behavior by only logging tracebacks and not storing them inside of tasks.
1 parent f14a9bb commit 7dfbdbe

File tree

12 files changed

+148
-38
lines changed

12 files changed

+148
-38
lines changed

CHANGES/+no-traceback-task.removal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stopped leaking sensitive information of failures in the task API.

pulpcore/app/models/task.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55
import logging
6-
import traceback
76
from gettext import gettext as _
87

98
from django.contrib.postgres.fields import ArrayField, HStoreField
@@ -261,7 +260,7 @@ def set_completed(self, result=None):
261260
)
262261
self._cleanup_progress_reports(TASK_STATES.COMPLETED)
263262

264-
def set_failed(self, exc, tb):
263+
def set_failed(self, exc):
265264
"""
266265
Set this Task to the failed state and save it.
267266
@@ -270,11 +269,9 @@ def set_failed(self, exc, tb):
270269
271270
Args:
272271
exc (Exception): The exception raised by the task.
273-
tb (traceback): Traceback instance for the current exception.
274272
"""
275273
finished_at = timezone.now()
276-
tb_str = "".join(traceback.format_tb(tb))
277-
error = exception_to_dict(exc, tb_str)
274+
error = exception_to_dict(exc)
278275
rows = Task.objects.filter(
279276
pk=self.pk,
280277
state=TASK_STATES.RUNNING,

pulpcore/app/tasks/repository.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from asgiref.sync import sync_to_async
99
from django.db import transaction
10-
from rest_framework.serializers import ValidationError
10+
from pulpcore.exceptions import RepositoryVersionDeleteError
1111

1212
from pulpcore.app import models
1313
from pulpcore.app.models import ProgressReport
@@ -44,12 +44,7 @@ def delete_version(pk):
4444
return
4545

4646
if version.repository.versions.complete().count() <= 1:
47-
raise ValidationError(
48-
_(
49-
"Cannot delete repository version. Repositories must have at least one "
50-
"repository version."
51-
)
52-
)
47+
raise RepositoryVersionDeleteError()
5348

5449
log.info(
5550
"Deleting and squashing version {num} of repository '{repo}'".format(

pulpcore/download/factory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import asyncio
33
import atexit
44
import copy
5-
from gettext import gettext as _
65
from multidict import MultiDict
76
import platform
87
import ssl
@@ -15,6 +14,7 @@
1514
from pulpcore.app.apps import PulpAppConfig
1615
from .http import HttpDownloader
1716
from .file import FileDownloader
17+
from pulpcore.exceptions import UrlSchemeNotSupportedError
1818

1919

2020
PROTOCOL_MAP = {
@@ -177,7 +177,7 @@ def build(self, url, **kwargs):
177177
builder = self._handler_map[scheme]
178178
download_class = self._download_class_map[scheme]
179179
except KeyError:
180-
raise ValueError(_("URL: {u} not supported.".format(u=url)))
180+
raise UrlSchemeNotSupportedError(url)
181181
else:
182182
return builder(download_class, url, **kwargs)
183183

pulpcore/download/http.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import aiohttp
44
import asyncio
55
import backoff
6+
import socket
67

78
from .base import BaseDownloader, DownloadResult
89
from pulpcore.exceptions import (
910
DigestValidationError,
1011
SizeValidationError,
1112
TimeoutException,
13+
DnsDomainNameException,
14+
ProxyAuthenticationFailedError,
1215
)
1316

1417

@@ -236,6 +239,7 @@ async def run(self, extra_data=None):
236239
aiohttp.ClientPayloadError,
237240
aiohttp.ClientResponseError,
238241
aiohttp.ServerDisconnectedError,
242+
DnsDomainNameException,
239243
TimeoutError,
240244
TimeoutException,
241245
DigestValidationError,
@@ -269,7 +273,7 @@ async def download_wrapper():
269273
e.message,
270274
)
271275
)
272-
raise e
276+
raise ProxyAuthenticationFailedError(self.proxy)
273277

274278
return await download_wrapper()
275279

@@ -289,12 +293,18 @@ async def _run(self, extra_data=None):
289293
"""
290294
if self.download_throttler:
291295
await self.download_throttler.acquire()
292-
async with self.session.get(
293-
self.url, proxy=self.proxy, proxy_auth=self.proxy_auth, auth=self.auth
294-
) as response:
295-
self.raise_for_status(response)
296-
to_return = await self._handle_response(response)
297-
await response.release()
296+
try:
297+
async with self.session.get(
298+
self.url, proxy=self.proxy, proxy_auth=self.proxy_auth, auth=self.auth
299+
) as response:
300+
self.raise_for_status(response)
301+
to_return = await self._handle_response(response)
302+
await response.release()
303+
except aiohttp.ClientConnectorError as e:
304+
# Check if this is a DNS error
305+
if isinstance(e.os_error, socket.gaierror):
306+
raise DnsDomainNameException(self.url)
307+
raise
298308
if self._close_session_on_finalize:
299309
await self.session.close()
300310
return to_return

pulpcore/exceptions/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
TimeoutException,
55
exception_to_dict,
66
DomainProtectedError,
7+
DnsDomainNameException,
8+
UrlSchemeNotSupportedError,
9+
ProxyAuthenticationFailedError,
10+
InternalErrorException,
11+
RepositoryVersionDeleteError,
712
)
813
from .validation import (
914
DigestValidationError,

pulpcore/exceptions/base.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ def exception_to_dict(exc, traceback=None):
4444
return {"description": str(exc), "traceback": traceback}
4545

4646

47+
class InternalErrorException(PulpException):
48+
"""
49+
Exception to signal that an unexpected internal error occurred.
50+
"""
51+
52+
def __init__(self):
53+
super().__init__("PLP0000")
54+
55+
def __str__(self):
56+
return _("An internal error occurred.")
57+
58+
4759
class ResourceImmutableError(PulpException):
4860
"""
4961
Exceptions that are raised due to trying to update an immutable resource
@@ -94,3 +106,73 @@ def __init__(self):
94106

95107
def __str__(self):
96108
return _("You cannot delete a domain that still contains repositories with content.")
109+
110+
111+
class DnsDomainNameException(PulpException):
112+
"""
113+
Exception to signal that dns could not resolve the domain name for specified url.
114+
"""
115+
116+
def __init__(self, url):
117+
"""
118+
:param url: the url that dns could not resolve
119+
:type url: str
120+
"""
121+
super().__init__("PLP0008")
122+
self.url = url
123+
124+
def __str__(self):
125+
return _("URL lookup failed.")
126+
127+
128+
class UrlSchemeNotSupportedError(PulpException):
129+
"""
130+
Exception raised when a URL scheme (e.g. 'ftp://') is provided that
131+
Pulp does not have a registered handler for.
132+
"""
133+
134+
def __init__(self, url):
135+
"""
136+
:param url: The full URL that failed validation.
137+
:type url: str
138+
"""
139+
super().__init__("PLP0009")
140+
self.url = url
141+
142+
def __str__(self):
143+
return _("URL: {u} not supported.").format(u=self.url)
144+
145+
146+
class ProxyAuthenticationError(PulpException):
147+
"""
148+
Exception to signal that the proxy server requires authentication
149+
but it was not provided or is invalid
150+
"""
151+
152+
def __init__(self, proxy_url):
153+
"""
154+
:param proxy_url: The URL of the proxy server.
155+
:type proxy_url: str
156+
"""
157+
super().__init__("PLP0010")
158+
self.proxy_url = proxy_url
159+
160+
def __str__(self):
161+
return _(
162+
"Proxy authentication failed for {proxy_url}. Please check your proxy credentials."
163+
).format(proxy_url=self.proxy_url)
164+
165+
166+
class RepositoryVersionDeleteError(PulpException):
167+
"""
168+
Raised when attempting to delete a repository version that cannot be deleted
169+
"""
170+
171+
def __init__(self):
172+
super().__init__("PLP0011")
173+
174+
def __str__(self):
175+
return _(
176+
"Cannot delete repository version. Repositories must have at least one "
177+
"repository version."
178+
)

pulpcore/tasking/tasks.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@
3030
TASK_WAKEUP_HANDLE,
3131
TASK_WAKEUP_UNBLOCK,
3232
)
33+
from pulpcore.exceptions.base import PulpException, InternalErrorException
34+
from pulp_glue.common.exceptions import PulpException as PulpGlueException
35+
3336
from pulpcore.middleware import x_task_diagnostics_var
3437
from pulpcore.tasking.kafka import send_task_notification
3538

39+
3640
_logger = logging.getLogger(__name__)
3741

3842

@@ -70,17 +74,22 @@ def _execute_task(task):
7074
log_task_start(task, domain)
7175
task_function = get_task_function(task)
7276
result = task_function()
77+
except (PulpException, PulpGlueException):
78+
# Log expected ways to fail without a stacktrace.
79+
exc_type, exc, _ = sys.exc_info()
80+
log_task_failed(task, exc_type, exc, None, domain)
81+
task.set_failed(exc)
7382
except Exception:
83+
# Unexpected Exceptions are most probably a programming error.
84+
# Log error with stack trace and return generic error (HTTP 500).
7485
exc_type, exc, tb = sys.exc_info()
75-
task.set_failed(exc, tb)
7686
log_task_failed(task, exc_type, exc, tb, domain)
77-
send_task_notification(task)
87+
safe_exc = InternalErrorException()
88+
task.set_failed(safe_exc)
7889
else:
7990
task.set_completed(result)
8091
log_task_completed(task, domain)
81-
send_task_notification(task)
82-
return result
83-
return None
92+
send_task_notification(task)
8493

8594

8695
async def aexecute_task(task):
@@ -95,17 +104,23 @@ async def _aexecute_task(task):
95104
try:
96105
task_coroutine_fn = await aget_task_function(task)
97106
result = await task_coroutine_fn()
107+
except (PulpException, PulpGlueException):
108+
# Log expected ways to fail without a stacktrace.
109+
exc_type, exc, _ = sys.exc_info()
110+
log_task_failed(task, exc_type, exc, None, domain)
111+
await sync_to_async(task.set_failed)(exc)
98112
except Exception:
113+
# Unexpected Exceptions are most probably a programming error.
114+
# Log error with stack trace and return generic error (HTTP 500).
99115
exc_type, exc, tb = sys.exc_info()
100-
await sync_to_async(task.set_failed)(exc, tb)
101116
log_task_failed(task, exc_type, exc, tb, domain)
102-
send_task_notification(task)
117+
safe_exc = InternalErrorException()
118+
await sync_to_async(task.set_failed)(safe_exc)
103119
else:
104120
await sync_to_async(task.set_completed)(result)
105121
send_task_notification(task)
106122
log_task_completed(task, domain)
107-
return result
108-
return None
123+
send_task_notification(task)
109124

110125

111126
def log_task_start(task, domain):
@@ -144,7 +159,8 @@ def log_task_failed(task, exc_type, exc, tb, domain):
144159
domain=domain.name,
145160
)
146161
)
147-
_logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb))))
162+
if tb:
163+
_logger.info("\n".join(traceback.format_list(traceback.extract_tb(tb))))
148164

149165

150166
async def aget_task_function(task):

pulpcore/tests/functional/api/test_tasking.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,9 @@ def test_executes_on_api_worker_when_no_async(
468468
"pulpcore.app.tasks.test.sleep", args=(LT_TIMEOUT,), immediate=True
469469
)
470470
monitor_task(task_href)
471-
assert "Immediate tasks must be async functions" in ctx.value.task.error["description"]
471+
# Assert masked internal error
472+
# Underlying cause is ValueError("Immediate tasks must be async functions")
473+
assert "An internal error occurred." in ctx.value.task.error["description"]
472474

473475
@pytest.mark.parallel
474476
def test_timeouts_on_api_worker(self, pulpcore_bindings, dispatch_task):
@@ -484,7 +486,8 @@ def test_timeouts_on_api_worker(self, pulpcore_bindings, dispatch_task):
484486
)
485487
task = pulpcore_bindings.TasksApi.read(task_href)
486488
assert task.state == "failed"
487-
assert "timed out after" in task.error["description"]
489+
# Assert masked internal error; underlying cause is asyncio.TimeoutError
490+
assert "An internal error occurred." in task.error["description"]
488491

489492

490493
@pytest.fixture
@@ -576,4 +579,5 @@ def test_times_out_on_task_worker(
576579
exclusive_resources=[COMMON_RESOURCE],
577580
)
578581
monitor_task(task_href)
579-
assert "timed out after" in ctx.value.task.error["description"]
582+
# Assert masked internal error; underlying cause is asyncio.TimeoutError
583+
assert "An internal error occurred." in ctx.value.task.error["description"]

pulpcore/tests/functional/api/using_plugin/test_proxy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def test_sync_https_through_http_proxy_with_auth_but_auth_not_configured(
101101
try:
102102
_run_basic_sync_and_assert(file_bindings, monitor_task, remote_on_demand, file_repo)
103103
except PulpTaskError as exc:
104-
assert "407, message='Proxy Authentication Required'" in exc.task.error["description"]
104+
assert "Proxy authentication failed for" in exc.task.error["description"]
105105

106106

107107
@pytest.mark.parallel

0 commit comments

Comments
 (0)