Skip to content

Commit edbaaf1

Browse files
committed
Limit the size of manifests/signatures during sync
Adds limit to the size of manifests and signatures as a safeguard to avoid DDoS attack during sync operations. To also prevent this during image upload, this commit configures a `client_max_body_size` for manifests and signatures Nginx endpoints. closes: #532
1 parent 5908f47 commit edbaaf1

File tree

5 files changed

+86
-5
lines changed

5 files changed

+86
-5
lines changed

CHANGES/532.feature

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added a limit of 4mb to the size of manifests and signatures as a safeguard to OOM DoS attack
2+
during sync tasks and updated the Nginx snippet to also limit the size of the body for these endpoints.

pulp_container/app/downloaders.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import aiohttp
22
import asyncio
3+
import fnmatch
34
import json
45
import re
56

@@ -10,7 +11,14 @@
1011

1112
from pulpcore.plugin.download import DownloaderFactory, HttpDownloader
1213

13-
from pulp_container.constants import V2_ACCEPT_HEADERS
14+
from pulp_container.constants import (
15+
MANIFEST_MEDIA_TYPES,
16+
MANIFEST_PAYLOAD_MAX_SIZE,
17+
MEGABYTE,
18+
SIGNATURE_PAYLOAD_MAX_SIZE,
19+
V2_ACCEPT_HEADERS,
20+
)
21+
from pulp_container.app.exceptions import InvalidRequest
1422

1523
log = getLogger(__name__)
1624

@@ -20,7 +28,41 @@
2028
)
2129

2230

23-
class RegistryAuthHttpDownloader(HttpDownloader):
31+
class ValidateResourceSizeMixin:
32+
async def validate_resource_size(self, response, request_method=None):
33+
"""
34+
Verify if the constrained resources are not exceeding the maximum size allowed.
35+
"""
36+
if request_method == "head":
37+
return
38+
39+
content_type = response.content_type
40+
max_resource_size = 0
41+
is_cosign_tag = fnmatch.fnmatch(response.url.name, "sha256-*.sig")
42+
43+
if isinstance(self, NoAuthSignatureDownloader) or is_cosign_tag:
44+
max_resource_size = SIGNATURE_PAYLOAD_MAX_SIZE
45+
content_type = "Signature"
46+
elif content_type in MANIFEST_MEDIA_TYPES.IMAGE + MANIFEST_MEDIA_TYPES.LIST:
47+
max_resource_size = MANIFEST_PAYLOAD_MAX_SIZE
48+
content_type = "Manifest"
49+
else:
50+
return
51+
52+
total_size = 0
53+
buffer = b""
54+
async for chunk in response.content.iter_chunked(MEGABYTE):
55+
total_size += len(chunk)
56+
buffer += chunk
57+
if total_size > max_resource_size:
58+
raise InvalidRequest(
59+
f"{content_type} size exceeded the {max_resource_size} bytes "
60+
f"limit ({total_size} bytes)."
61+
)
62+
response.content.unread_data(buffer)
63+
64+
65+
class RegistryAuthHttpDownloader(HttpDownloader, ValidateResourceSizeMixin):
2466
"""
2567
Custom Downloader that automatically handles Token Based and Basic Authentication.
2668
@@ -77,6 +119,7 @@ async def _run(self, handle_401=True, extra_data=None):
77119
async with session_http_method(
78120
self.url, headers=headers, proxy=self.proxy, proxy_auth=self.proxy_auth
79121
) as response:
122+
await self.validate_resource_size(response, http_method)
80123
try:
81124
response.raise_for_status()
82125
except ClientResponseError as e:
@@ -193,7 +236,7 @@ async def _handle_head_response(self, response):
193236
)
194237

195238

196-
class NoAuthSignatureDownloader(HttpDownloader):
239+
class NoAuthSignatureDownloader(HttpDownloader, ValidateResourceSizeMixin):
197240
"""A downloader class suited for signature downloads."""
198241

199242
def raise_for_status(self, response):
@@ -208,6 +251,20 @@ def raise_for_status(self, response):
208251
else:
209252
response.raise_for_status()
210253

254+
async def _run(self, extra_data=None):
255+
if self.download_throttler:
256+
await self.download_throttler.acquire()
257+
async with self.session.get(
258+
self.url, proxy=self.proxy, proxy_auth=self.proxy_auth, auth=self.auth
259+
) as response:
260+
await self.validate_resource_size(response)
261+
self.raise_for_status(response)
262+
to_return = await self._handle_response(response)
263+
await response.release()
264+
if self._close_session_on_finalize:
265+
await self.session.close()
266+
return to_return
267+
211268

212269
class NoAuthDownloaderFactory(DownloaderFactory):
213270
"""

pulp_container/app/registry_api.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@
9191
EMPTY_BLOB,
9292
SIGNATURE_API_EXTENSION_VERSION,
9393
SIGNATURE_HEADER,
94-
SIGNATURE_PAYLOAD_MAX_SIZE,
9594
SIGNATURE_TYPE,
9695
V2_ACCEPT_HEADERS,
9796
)
@@ -1426,7 +1425,7 @@ def put(self, request, path, pk):
14261425
except models.Manifest.DoesNotExist:
14271426
raise ManifestNotFound(reference=pk)
14281427

1429-
signature_payload = request.META["wsgi.input"].read(SIGNATURE_PAYLOAD_MAX_SIZE)
1428+
signature_payload = request.META["wsgi.input"].read()
14301429
try:
14311430
signature_dict = json.loads(signature_payload)
14321431
except json.decoder.JSONDecodeError:

pulp_container/app/webserver_snippets/nginx.conf

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,25 @@ location /token/ {
3838
proxy_redirect off;
3939
proxy_pass http://pulp-api;
4040
}
41+
42+
location ~* /v2/.*/manifests/.*$ {
43+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44+
proxy_set_header X-Forwarded-Proto $scheme;
45+
proxy_set_header Host $http_host;
46+
# we don't want nginx trying to do something clever with
47+
# redirects, we set the Host: header above already.
48+
proxy_redirect off;
49+
proxy_pass http://pulp-api;
50+
client_max_body_size 4m;
51+
}
52+
53+
location ~* /extensions/v2/.*/signatures/.*$ {
54+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
55+
proxy_set_header X-Forwarded-Proto $scheme;
56+
proxy_set_header Host $http_host;
57+
# we don't want nginx trying to do something clever with
58+
# redirects, we set the Host: header above already.
59+
proxy_redirect off;
60+
proxy_pass http://pulp-api;
61+
client_max_body_size 4m;
62+
}

pulp_container/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,6 @@
6969

7070
MEGABYTE = 1_000_000
7171
SIGNATURE_PAYLOAD_MAX_SIZE = 4 * MEGABYTE
72+
MANIFEST_PAYLOAD_MAX_SIZE = 4 * MEGABYTE
7273

7374
SIGNATURE_API_EXTENSION_VERSION = 2

0 commit comments

Comments
 (0)