Skip to content
This repository has been archived by the owner on Dec 7, 2022. It is now read-only.

Safer handling of Accept headers #84

Merged
merged 1 commit into from Jan 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -6,3 +6,4 @@ Dennis Kliban (dkliban@redhat.com)
Ina Panova (ipanova@redhat.com)
Aaron Weitekamp (aweiteka@redhat.com)
A.P. Rajshekhar (randalap@redhat.com)
Mihai Ibanescu (mihai.ibanescu@gmail.com)
30 changes: 28 additions & 2 deletions crane/views/v2.py
@@ -1,12 +1,13 @@
from __future__ import absolute_import
import httplib
import logging
import os
from flask import Blueprint, json, current_app, redirect, request

from crane import app_util, exceptions
from crane.api import repository


log = logging.getLogger('__name__')
section = Blueprint('v2', __name__, url_prefix='/v2')


Expand Down Expand Up @@ -77,7 +78,7 @@ def name_redirect(relative_path):
if schema2_data or manifest_list_data:
# if it is a newer docker client it sets accept headers to manifest schema 1, 2 and list
# if it is an older docker client, he doesnot set any of accept headers
accept_headers = request.headers.get('Accept')
accept_headers = get_accept_headers(request)
schema2_mediatype = 'application/vnd.docker.distribution.manifest.v2+json'
manifest_list_mediatype = 'application/vnd.docker.distribution.manifest.list.v2+json'
# check first manifest list type
Expand Down Expand Up @@ -125,3 +126,28 @@ def handle_error(error):
response.headers['Content-Type'] = 'application/json'
response.status_code = error.status_code
return response


def get_accept_headers(request):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add doc block

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

"""
Parse the Accept: request header and return a set of media types.

WSGI will turn multiple Accept: headers into a comma-separated string,
which is expected according to HTTP standards.

https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

:param request: flask request object for a request
:type request: flask.Request

:return: set of Accept: headers
:rtype: set
"""
accept_headers = request.headers.get('Accept')
log.debug("Accept headers from client: %s", accept_headers)
if not accept_headers:
return set()
accept_headers = accept_headers.split(',')
# Accept headers may contain additional quality parameters after ;
# We will simply discard that for now
return set(x.partition(';')[0].strip() for x in accept_headers)
9 changes: 6 additions & 3 deletions tests/views/test_path.py
Expand Up @@ -14,12 +14,15 @@ def test_invalid_repo_name(self):
self.assertEqual(parsed_response_data['errors'][0]['message'], 'Not Found')

def test_valid_repo_name_for_manifest(self):
headers = {'Accept': 'application/vnd.docker.distribution.manifest.v2+json'}
response = self.test_client.get('/v2/redhat/foo/manifests/1.25.1-musl', headers=headers)
# #3303: verify multi-valued headers too
# manifest lists are evaluated first, so pass a longer media type that
Copy link
Member

@ipanova ipanova Jan 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you elaborate more on this please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code first checks if manifest_list_mediatype is in accept_headers. If the client sent a string that includes 'application/vnd.docker.distribution.manifest.list.v2+json like in my +junk test, as well as a valid manifest v2 media type, then you'd get a false match for a manifest list.

# matches the manifest list as a prefix
headers = {'Accept': 'application/vnd.docker.distribution.manifest.list.v2+jsonjunk,application/vnd.docker.distribution.manifest.v2+json'} # noqa
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think +jsonjunk would be a valid mediatype sent from docker client :) max what you can have is +json or +prettyjws

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's with a civilized client. As a server, you should always check the inputs from the client. This is not a realistic example, just like this patch is not fixing something broken now.

response = self.test_client.get('/v2/redhat/zoo/manifests/1.25.1-musl', headers=headers)

self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers['Content-Type'].startswith('text/html'))
self.assertTrue('foo/bar/manifests/2' in response.headers['Location'])
self.assertTrue('zoo/bar/manifests/2/1.25.1-musl' in response.headers['Location'])

def test_valid_repo_name_for_manifest_list(self):
headers = {'Accept': 'application/vnd.docker.distribution.manifest.list.v2+json'}
Expand Down
20 changes: 20 additions & 0 deletions tests/views/test_v2.py
@@ -0,0 +1,20 @@
import mock
import unittest2

from crane.views import v2


class UtilTest(unittest2.TestCase):
def test_get_accept_headers(self):
tests = [
(dict(), set()),
(dict(Accept="a"), set(["a"])),
(dict(Accept="a, b"), set(["a", "b"])),
(dict(Accept="a,b"), set(["a", "b"])),
(dict(Accept=" a , b "), set(["a", "b"])),
(dict(Accept="a; q=1, b"), set(["a", "b"])),
]
req = mock.MagicMock()
for headers, expected in tests:
req.headers = headers
self.assertEquals(expected, v2.get_accept_headers(req))