Git Product home page Git Product logo

audit's People

Contributors

sveeke avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

audit's Issues

The "name" parameter of the award a badge functionality lacks any input validation

threatLevel="Low" type="User input validation"

The "name" parameter of the award a badge functionality lacks any input validation:

image

Example request:

POST /v1/issuer/issuers/rwygWIDnR1uBB_i1MPwu_g/badges/XjAr-XCzRDyL7PxOJZSZqA/assertions HTTP/1.1
Host: badgr-dev2.edubadges.nl
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Token 5a29a471f3b21be11928361f5c42aeabf0c5cd8f
Content-Type: application/json
Referer: https://surf-dev2.edubadges.nl/issuer/issuers/rwygWIDnR1uBB_i1MPwu_g/badges/XjAr-XCzRDyL7PxOJZSZqA/issue
Content-Length: 442
Origin: https://surf-dev2.edubadges.nl
Connection: close

{"issuer":"rwygWIDnR1uBB_i1MPwu_g","badge_class":"XjAr-XCzRDyL7PxOJZSZqA","recipient_type":"email","recipient_identifier":"[email protected]","narrative":"","create_notification":true,"evidence_items":[],"extensions":{"extensions:recipientProfile":{"@context":"https://openbadgespec.org/extensions/recipientProfile/context.json","type":["Extension","extensions:RecipientProfile"],"name":"<script>alert('blaat');</script>Stefan"}}}

Response:

{
<KNIP>
  "extensions": {
    "extensions:recipientProfile": {
      "@context": "https://openbadgespec.org/extensions/recipientProfile/context.json",
      "type": [
        "Extension",
        "extensions:RecipientProfile"
      ],
      "name": "<script>alert('blaat');</script>Stefan"
    }
  },
<KNIP>

It did not result in a successful XSS as the content-type is application/json and not text/html. The contents of the name field seemed not to be requested at other places on the website that use text/html as the content-type and is also not send to the user in the confirmation e-mail.

impact:
Not validating the user input could result in issues such as XSS.

recommendation:
Filter all client-provided input parameters, and escape all output
Make use of a whitelist, and prevent certain characters from being inserted if it's not necessary for the working of the application.

Python Plugin Version leaked and outdated

The Python Request Plugin used to add badges locates on other webservers appears to be outdated.

The version 2.18.1 that is revealed in the User-Agent header was released on Jun 14, 2017.

omega# nc -nlvp 80
listening on [any] 80 ...
connect to [5.2.67.184] from (UNKNOWN) [145.101.112.185] 49920
GET / HTTP/1.1
Host: omega.svits.nl
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: application/ld+json, application/json, image/png, image/svg+xml
User-Agent: python-requests/2.18.1

The Add Url Option of the Assign Badge functionality Allows All Urls

The Add Url Option of the Assign Badge functionality does not use a whitelist to restrict from which urls it could load badge images from.

The following request was send to our test host that was listening for an incoming request on port 80

Request:

POST /v1/earner/badges?json_format=plain HTTP/1.1
Host: badgr-dev2.edubadges.nl
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Token 820bf48af092a422a5e37fd2805daf03d78bd48d
Content-Type: application/json
Referer: https://surf-dev2.edubadges.nl/recipient/badges
Content-Length: 31
Origin: https://surf-dev2.edubadges.nl
Connection: close

{"url":"http://omega.svits.nl"}
omega# nc -nlvp 80
listening on [any] 80 ...
connect to [5.2.67.184] from (UNKNOWN) [145.101.112.185] 49920
GET / HTTP/1.1
Host: omega.svits.nl
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept: application/ld+json, application/json, image/png, image/svg+xml
User-Agent: python-requests/2.18.1

The request was received by our testserver.

Impact:
With the current installed version of the Python request plugin no vulnerabilites are known that could result in exploitation. However allowing all domains to interact would still increase the attack vector.

Recommendation:

  • Use a domain whitelist.

Admin can delete protected items on the admin UI

The delete_selected admin action can be tricked to delete protected objects.

Excerpt from apps/mainsite/admin_actions.py:34

if request.POST.get('post'):

This action is a fork of the original django implementation, and it
seems it did not apply patches or had this check removed:

if request.POST.get('post') and not protected:

Protected objects are foreign keys that can be marked as protected in the ORM model:
https://docs.djangoproject.com/en/2.1/ref/models/fields/#django.db.models.PROTECT

Prevent deletion of the referenced object by raising ProtectedError, a subclass of django.db.IntegrityError.

In Openbadges two foreign keys are marked protected:

  • LocalBadgeClass sets its Issuer FK as protected.
  • LocalBadgeInstance sets its LocalBadgeClass to protected.

Excerpts from apps/composition/models.py:

class LocalBadgeClass(AbstractRemoteImagePreviewMixin, AbstractBadgeClass):
    issuer = models.ForeignKey(LocalIssuer, blank=False, null=False,
                               on_delete=models.PROTECT,
                               related_name="badgeclasses")

And LocalBadgeInstance which points back to LocalBadgeClass as protected:

class LocalBadgeInstance(AbstractBadgeInstance):
    # 0.5 BadgeInstances have no notion of a BadgeClass
    badgeclass = models.ForeignKey(LocalBadgeClass, blank=False, null=True,
                                   on_delete=models.PROTECT,
                                   related_name='badgeinstances')

Impact: someone with admin rights can delete Issuers that are still
being referenced by other objects putting the database in an
inconsistent state.

Recommendation add the missing check for protected status.

Cipher Order Determined by Client

The badgr-dev2.edubadges.nl server allows the SSL/TLS cipher to be chosen by the client instead of the server. This could result in a less-than-optimal encryption algorithm being chosen for the encryption of sensitive data.

Enumeration of user ids in API endpoint BadgeUserEmailDetail

It is possible to enumerate the user ids using this simple oracle in /user/emails/<id>:

Excerpt from class BadgeUserEmailDetail():

        email_address = self.get_email(pk=id)
        if email_address is None:
            return Response(status=status.HTTP_404_NOT_FOUND)
        if email_address.user_id != self.request.user.id:
            return Response(status=status.HTTP_403_FORBIDDEN)

Impact: an attacker can find out which user ids are valid, this could be used in later steps of a more complex attack.

Recommendation: Return the same error code in both cases.

Improve Input Validation and output Sanitization.

threatLevel="Low" type="User input validation"

The current implementation of input validation and output sanitization of the application removes tags such as <script>bla</script> but allows dangerous characters such as < > " ' / when there is a leading space: < script>bla< /script>.

A leading space does not result in a valid XSS but it is recommended to strip dangerous characters and escape them properly to decrease the attack vector.

Example request that contains the dangerous characters:

POST /v1/issuer/issuers HTTP/1.1
Host: badgr-dev2.edubadges.nl
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Token f58408e93d4a503f9e66f9103775a3c104bf8189
Content-Type: application/json
Referer: https://surf-dev2.edubadges.nl/issuer/create
Content-Length: 146
Origin: https://surf-dev2.edubadges.nl
Connection: close

{"name":" <  >  \"  '  / ","description":"fff\n <  >  \"  '  / ","email":"[email protected]","url":"https://www.edubadges.nl"}

Response:

POST /v1/issuer/issuers HTTP/1.1
Host: badgr-dev2.edubadges.nl
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04
Accept: application/json
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Authorization: Token f58408e93d4a503f9e66f9103775a3c104bf8189
Content-Type: application/json
Referer: https://surf-dev2.edubadges.nl/issuer/create
Content-Length: 146
Origin: https://surf-dev2.edubadges.nl
Connection: close

{"name":" <  >  \"  '  / ","description":"fff\n <  >  \"  '  / ","email":"[email protected]","url":"https://www.edubadges.nl"}

impact:
Not validating the user input could result in issues such as XSS.

recommendation:
Filter all client-provided input parameters, and escape all output
Make use of a whitelist, and prevent certain characters from being inserted if it's not necessary for the working of the application.

Web Browser XSS Protection Not Enabled

The X-XSS-Protection HTTP Header is not set on surf-dev2.edubadges.nl and badgr-dev2.edubadges.nl. This header enables the Cross-Site Scripting (XSS) filter built into most recent web browsers.

Unhandled Division by Zero

The jingo template helper function divisible_by in
apps/mainsite/helpers.py does not check for the divisor not being
zero and can thus cause an exception.

def divisible_by(value, num):
    """Check if a variable is divisible by a number."""
    return value % num == 0

Recommendation: This helper function is seemingly not used in the
templates, so it can be removed, or alternatively it can check for
num!=0.

Frameable response (potential Clickjacking)

The application surf-dev2.edubadges.nl fails to set an appropriate X-Frame-Options or Content-Security-Policy HTTP header, it is possible for a page controlled by an attacker to load it within an iframe.

Django Debug mode reveals information about the code and infrastructure.

threatLevel="Moderate" type="Insufficiently Hardened Server"

The Django Debug mode is enabled which reveals information about the code and infrastructure.

Example:


Page not found (404)
Request Method: 	GET
Request URL: 	http://badgr-dev2.edubadges.nl/'

Using the URLconf defined in mainsite.urls, Django tried these URL patterns, in this order:

    ^component-library$ [name='component-library']
    ^static/(?P<path>.*)
    ^media/(?P<path>.*)$
    ^favicon\.png[/]?$
    ^favicon\.ico[/]?$
    ^robots\.txt$
    ^static/images/header-logo-120.png$
    ^apple-app-site-association [name='apple-app-site-association']
    ^o/authorize/?$ [name='oauth2_api_authorize']
    ^o/token/?$ [name='oauth2_provider_token']
    ^o/
    ^$ [name='index']
    ^accounts/login/$ [name='legacy_login_redirect']
    ^staff/sidewide-actions$ [name='badgr_admin_sitewide_actions']
    ^staff/
    ^health
    ^docs/oauth2/authorize$ [name='docs_authorize_redirect']
    ^docs/?$
    ^docs/
    ^json-ld/
    ^unsubscribe/(?P<email_encoded>[^/]+)/(?P<expiration>[^/]+)/(?P<signature>[^/]+) [name='unsubscribe']
    ^public/
    ^public/
    ^share/?collection/(?P<share_hash>[^/]+)(/embed)?$ [name='redirect_backpack_shared_collection']
    ^share/?badge/(?P<share_hash>[^/]+)$ [name='legacy_redirect_backpack_shared_badge']
    ^earner/collections/(?P<pk>[^/]+)/(?P<share_hash>[^/]+)$ [name='legacy_shared_collection']
    ^earner/collections/(?P<pk>[^/]+)/(?P<share_hash>[^/]+)/embed$ [name='legacy_shared_collection_embed']
    ^api-auth/token$
    ^account/
    ^v1/user/
    ^v1/user/
    ^v1/issuer/
    ^v1/earner/
    ^v2/issuers/(?P<issuer_slug>[^/]+)/pathways
    ^v2/
    ^v2/
    ^v2/
    ^v2/backpack/
    ^v1/externaltools/
    ^v2/externaltools/
    ^v2/

The current URL, ', didn't match any of these.

You're seeing this error because you have DEBUG = True in your Django settings file. Change that to False, and Django will display a standard 404 page.


Another example:

https://badgr-dev2.edubadges.nl/v1/earner/share/collection/sTjBTvTOSWGIu8TNhO7R6Q?provider=Facebookkgjr4%3cscript%3ealert(1)%3c%2fscript%3egjwj1&source=badgr-ui&redirect=0

Condensed output:

DATABASES 	

{'default': {'ATOMIC_REQUESTS': False,
             'AUTOCOMMIT': True,
             'CONN_MAX_AGE': 0,
             'ENGINE': 'django.db.backends.mysql',
             'HOST': 'db',
             'NAME': 'badgr',
             'OPTIONS': {'init_command': 'SET default_storage_engine=InnoDB'},
             'PASSWORD': u'********************',
             'PORT': '3306',
             'TEST': {'CHARSET': None,
                      'COLLATION': None,
                      'MIRROR': None,
                      'NAME': None},
             'TIME_ZONE': None,
             'USER': 'badgr_user'}}

Impact:
This increases the attack vector as the output gives the attacker more insight about the infrastructure, software and code that will increase the change of a successful attack.

Recommendation:
Disable the debug mode when not used.
Don't enable the debug mode on a production server.

Development and deprecated modules unconditionally enabled

The django settings of the mainsite app enable some modules that are either deprecated or only needed for debugging and testing, especially the latter ones can leak information and or provide other facilities to an attacker. They increase the attack-surface unnecessarily.

Excerpt from apps/mainsite/settings.py:

     jingo
...
     #extradditions for debugging and testing
    'taggit',
    'taggit_serializer',
    'django_extensions',
    'sslserver',
...
    # deprecated packages to remove in v1.2
    'composer',
    'credential_store',

A quick check on github shows a lot of open bugs some of which can possibly be abused:

  • django-taggit: has 97 open issues on github
  • django-taggit-serializer: has 17 open issues on github
  • django-rest-swagger: has 91 open issues on github
  • jingo, from its README: "Jingo is DEPRECATED"

Recommendation: Remove all unnecessary modules from settings.py, or only enable them conditionally in a settings_local.py.

Outdated Nginx webservers installed

threatLevel="Low"
type="Outdated Software"

The following Nginx webservers are outdated and reveal their version number in the banner:

lti-dev2.edubadges.nl (145.101.112.188) - nginx 1.13.12 (released on 11/04/2018)
badgr-dev2.edubadges.nl (145.101.112.185) - nginx/1.12.2 (released on 18/10/2017)
surf-dev2.edubadges.nl (145.101.112.186) - nginx/1.12.2 (released on 18/10/2017)

Impact:
Although no security issues were found it is best practice to use the latest stable version. Also hiding the version number in the banner would make it more time consuming for an attacker to determine if a vulnerable version is installed or not.

Recommendation:
Upgrade to the latest version.
Have a good update policy implemented.
Hide the servername and especially version number in the banner.

Enumeration of registered email addresses via user profile API

BadgeUserProfile uses BadgeUserProfileSerializer which responds to registration attempts with this error message:

Account could not be created. An account with this email address may already exist.

This API endpoint can be accessed by sending a POST request to v1/user/profile. It expects these fields as parameters:

first_name = serializers.CharField(max_length=30, allow_blank=True)                
last_name = serializers.CharField(max_length=30, allow_blank=True)                 
email = serializers.EmailField()                                                   
password = serializers.CharField(style={'input_type': 'password'}, write_only=True)

Badge Check can be fooled by forged badges using unicode domain names

Currently v0.5 and v1 badges only verify by is this:

        if not (domain(self.issuer['origin']) ==
                domain(self.instance_url)):  # TODO: Can come from baked image

So it is possible to fake badges with unicode shenanigans, maybe also setting up the domain to return expected responses, but BadgeCheck itself does not initiate network connections.

in V1 badgecheck there is also a check_components_have_same_domain
but it is not used anywhere, and has a check:

        resources = filter(None, [self.badge_instance['verify']['url'],
                                  self.badge_instance['badge'],
                                  self.badge_instance.get('url'),
                                  self.badge_instance.get('id')])
        same_domains = len(set([domain(resource)
                                for resource in resources])) == 1

which can be fooled by a badge that has only one of the resources non-None.

Example: badges.stanford.edu could be used as a domain, for a human it would be very difficult to realize that the a in this domain is a cyrillic lower case a letter and not the latin a.

Impact: The current implementation would verify this as a valid badge, and a human would easily mistake this as a badge by a prestigious institution.

Recommendation: always convert unicode domains into punycode.

Missing Terms of Service and Privacy Policy

threatLevel="Low" type="Privacy"

The website does not have a Terms of Service and Privacy Policy. The links on the bottom of the page do not work. The Privacy Policy should be added when the website goes live or when it contains private personal information.

Recommendation:
Add a privacy policy.

Impact:
Legislation issues when dealing with private personal information.

SSH Server on surf-dev2.edubadges.nl has CBC Mode Ciphers Enabled

threatLevel="Low" type="Insecure SSL/TLS Configuration"

The SSH server is configured to support Cipher Block Chaining (CBC) encryption.

To verify the issue:

nmap -sT -sV -p22 surf-dev2.edubadges.nl --script=ssh2-enum-algos -Pn

Output shows:

The following client-to-server Cipher Block Chaining (CBC) algorithms
are supported : 

  3des-cbc
  aes128-cbc
  aes192-cbc
  aes256-cbc
  blowfish-cbc
  cast128-cbc

The following server-to-client Cipher Block Chaining (CBC) algorithms
are supported : 

  3des-cbc
  aes128-cbc
  aes192-cbc
  aes256-cbc
  blowfish-cbc
  cast128-cbc

Impact:
This may allow an attacker to recover the plaintext message from the ciphertext.

Recommendation:
Disable CBC mode cipher encryption, and enable CTR or GCM cipher mode encryption.

Arbitrary file upload with arbitrary file-extensions in images of badges.

It is possible for an attacker to upload arbitrary files with arbitrary extensions that are publicly accessible.
This could be a javascript payload, or other content used in more complex attacks.

LocalBadgeInstanceUploadSerializer.create() creates badges with an image like this:

new_instance, instance_created = LocalBadgeInstance.objects.get_or_create({
...
    'image': use_or_bake_badge_instance_image(validated_data.get('image'), badge_instance, badge_class)
}, identifier=badge_instance_url, recipient_user=request_user)

The image is set by running use_or_bake_badge_instance_image() which contains:

if uploaded_image and verify_baked_image(uploaded_image):
    baked_badge_instance = uploaded_image  # InMemoryUploadedFile

it then stores our payload like this with our own file extension:

_, image_extension = os.path.splitext(baked_badge_instance.name)
baked_badge_instance.name = \
    'local_badgeinstance_' + str(uuid.uuid4()) + image_extension
return baked_badge_instance

How can we fool this check verify_baked_image() to store our payload?

def verify_baked_image(uploaded_image):
    try:
        unbake(uploaded_image)
    except Exception:
        return False

    return True

It seems we only need to run unbake() without throwing an exception:

def unbake(image_file):
    """
    Return the openbadges content contained in a baked image.
    """
    image_type = check_image_type(image_file)
    image_file.seek(0)
    if image_type == 'PNG':
        return png_bakery.unbake(image_file)
    elif image_type == 'SVG':
        return svg_bakery.unbake(image_file)

To avoid running too much checks we only need to make sure that the
filetype is neither identified as PNG nor SVG, this check is done like
this:

def check_image_type(image_file):
    if image_file.read(8) == '\x89PNG\r\n\x1a\n':
        return 'PNG'
    image_file.seek(0)
    # TODO: Use xml library to more accurately detect SVG documents
    if re.search('<svg', image_file.read(256)):
        return 'SVG'

So our payload cannot start with a PNG header and cannot contain
<svg in the first 256 bytes, that is quite possible for our payload.

We can trigger LocalBadgeInstanceUploadSerializer.create() by
sending a POST request to LocalBadgeInstanceList at
/v1/earner/badges and providing a correct payload in the image
parameter that has a .js extension and javascript content.

Recommendations:

  • force the correct file extension,
  • serve from unrelated domain,
  • validate the contents of the images,
  • filter out javascript.

Pathway*list can be created by anyone with a registered email

The POST verb of PathwayList seems to allow anyone with a verified email to create new pathways for issuers? The POST verb of PathwayElementList also seems to be affected by this?

the corresponding API endpoints are

  • /v2/issuers/(?P<issuer_slug>[^/]+)/pathways/
  • /v2/issuers/(?P<issuer_slug>[^/]+)/pathways/(?P<pathway_slug>[^/]+)/elements

It seems the only authorization is needed that the user is logged in and has a confirmed email address.

Timing-side channel in API helps testing if an email address is registered

In the badgeuser API BadgeUserForgotPassword() the post() function
initiates a password reset procedure. It claims in a comment:

if email_address is None:
    # return 200 here because we don't want to expose information about which emails we know about
    return Response(status=status.HTTP_200_OK)

and later

try:
    user = UserCls.objects.get(pk=email_address.user_id)
except UserCls.DoesNotExist:
    return Response(status=status.HTTP_200_OK)

and then later when everything is alright:

temp_key = default_token_generator.make_token(user)
token = "{uidb36}-{key}".format(uidb36=user_pk_to_url_str(user),
                                key=temp_key)
reset_url = "{}{}?token={}".format(OriginSetting.HTTP, reverse('user_forgot_password'), token)

email_context = {
    "site": get_current_site(request),
    "user": user,
    "password_reset_url": reset_url,
}
get_adapter().send_mail('account/email/password_reset_key', email, email_context)

return Response(status=status.HTTP_200_OK)

The problem is that the early HTTP_200_OK responses create a timing side-channel. Admittedly it is a bit noisy since the target gets a password reset email.

XSS code injection via Composition Collection share_url

In the collection app the class CollectionSerializer contains a field:

share_url = serializers.CharField(read_only=True, max_length=1024)

Which despite being an URL it is not validated against the URLField rules.

CollectionSerializer is being used by the following writing API endpoints:

  • CollectionDetail - PUT v1/earner/collections/(?P<slug>[-\w]+)$
  • CollectionList - POST v1/earner/collections$

Later this share_url member variable is used in templates/composition/collection_detail_embed.html in the following way:

42:                            <h4><a href="{{collection.share_url}}">{{collection.name}}</a></h4>
95:                    <p class="embed-info">This badge collection verified by <a href="https://badgr.io">Badgr</a>. <a href="{{collection.share_url}}">View details</a>.</p>

In both cases share_url could be used to inject javascript to the client, by for example specifying javascript:alert(1) as the share_url.

Recommendation: Change the type of the share_url field to a URLField so it gets validated as a URL.

untrusted XML parsed with xml.dom.minidom.parseString

apps/bakery/svg_bakery.py uses xml.dom.minidom.parseString in two locations to parse SVG files, and this makes it vulnerable to billion laughs and quadratic blowup attacks. For more information see https://docs.python.org/3/library/xml.html#xml-vulnerabilities

excerpt from edubadges/apps/bakery/svg_bakery.py:10

9	
10	    svg_doc = parseString(imageFile.read())
11	    imageFile.close()

excerpt from edubadges/apps/bakery/svg_bakery.py:52

51	def unbake(imageFile):
52	    svg_doc = parseString(imageFile.read())
53	

It is recommened to use the defusedxml package instead.

Authentication Token In URL

A user retrieves an Authentication Token after a successful login. In some requests this token is leaked in the GET parameter and are vulnerable to disclosure.

ResizeUploadedImage possible server and client-side Resource Exhaustion Vulnerability

The ResizeUploadedImage mixin opens attacker supplied images naively
which can lead to a resource exhaustion on the server. Furthermore if
the resize operation fails it saves the image without changes and
possibly serves this imagebomb also to clients causing a resource
exhaustion to the browsers trying to display this image.

try:
    image = Image.open(self.image)
except IOError:
    return super(ResizeUploadedImage, self).save(*args, **kwargs)

This mixin is used in the AbstractIssuer and the AbstractBadgeClass.

Attached are a PNG and a JPG image that are crafted image bombs to
verify this issue.

Recommendation: use the Image._decompression_bomb_check() function
to filter out maliciously crafted images. Or consider upgrading to something >5.0.0: https://pillow.readthedocs.io/en/5.1.x/releasenotes/5.0.0.html?highlight=bomb

Hardcoded Unsubscribe token in settings.py

The application generates unsubscription tokens using a hard-coded secret in apps/mainsite/settings.py. The algorithm to generate that token is easily reversible, if the secret is public. This also allows an attacker to unsubscribe recipients but also to check if an email address is registered.

Excerpt from apps/mainsite/models.py for unsubscribe/verification as follows:

        secret_key = settings.UNSUBSCRIBE_SECRET_KEY

        expiration = datetime.utcnow() + timedelta(days=7)  # In one week.
        timestamp = int((expiration - datetime(1970, 1, 1)).total_seconds())

        email_encoded = base64.b64encode(email)
        hashed = hmac.new(secret_key, email_encoded + str(timestamp), sha1)

This can be cheaply reversed by testing an unsubscribe token using the following example:

#!/usr/bin/env python

from datetime import datetime, timedelta
from hashlib import sha1
import base64
import hmac

# https://badgr-dev2.edubadges.nl/unsubscribe/c3RlZmFucGVudGVzdCtzdHVkZW50QGdtYWlsLmNvbQ==/1529190185/074489b0ad6d1d5bbef667ce768220f5151da268

UNSUBSCRIBE_SECRET_KEY = 'kAYWM0YWI2MDj/FODBZjE0ZDI4N'
email='[email protected]'
target = '074489b0ad6d1d5bbef667ce768220f5151da268'

secret_key = UNSUBSCRIBE_SECRET_KEY

expiration = datetime.utcnow() + timedelta(days=7)  # In one week.
#timestamp = int((expiration - datetime(1970, 1, 1)).total_seconds())
ts=1529190185
email_encoded = base64.b64encode(email)

hashed = hmac.new(secret_key, email_encoded + str(ts), sha1)
if hashed.hexdigest() == target:
    print "confirmed using known UNSUBSCRIBE_SECRET_KEY"
    print "token expiring at: ", datetime.fromtimestamp(ts).isoformat()
else:
    print "probably not using known UNSUBSCRIBE_SECRET_KEY"

In the case of the tested site the token returned tests positively for the known secret.

Recommendation: use the SECRET_KEY required and checked by the Django framework.

No Bruteforce Protection on Account Login

threatLevel=Low type=Missing Bruteforce Protection

There is no bruteforce protection against the bruteforce of accounts at the Login Portal.

Example of a login request:

POST /api-auth/token HTTP/1.1
Host: badgr-dev2.edubadges.nl
User-Agent: Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.04
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Referer: https://surf-dev2.edubadges.nl/auth/login
Content-Length: 58
Origin: https://surf-dev2.edubadges.nl
Connection: close

username=stefanpentest%2Bteacher%40gmail.com&password=test

HTTP/1.1 400 Bad Request
Server: nginx/1.12.2
Date: Thu, 07 Jun 2018 02:32:13 GMT
Content-Type: application/json
Connection: close
Vary: Authorization, Cookie
X-Frame-Options: ALLOW-FROM HTTP://CANVAS.EDUBADGES.NL/, HTTPS://CANVAS.EDUBADGES.NL
Access-Control-Allow-Origin: *
Allow: POST, OPTIONS
Content-Length: 68

{"non_field_errors":["Unable to log in with provided credentials."]}

impact:
Lack of brute force protection on the login mechanism may lead to account theft as the attacker is not blocked in bruteforcing possible passwords.

recommendation:
Implement a limit on login attempts with the use of Captcha (f.i. https://www.google.com/recaptcha is very effective) after three login attempts.
Enforce an account and IP lockout (or ban) if there are too many login attempts in a short period of time.

Upload files with arbitrary extensions to publicly accessible URL

The AbstractRemoteImagePreviewMixin allows an attacker to upload
files with arbitrary extension to the webserver, which can be used as
building-blocks for more complex attacks, also this can be abused to upload
image decompression bombs which could be used to DoS client browsers.

Excerpt from apps/mainsite/models.py:

class AbstractRemoteImagePreviewMixin(models.Model):
...
    @property
    def image_preview(self):
        remote_url = self.json.get('image', None)
        if remote_url and self.image_preview_status is None and not self.image:
            # attempt to cache a local copy if we haven't tried before
            if remote_url is not None:
                store = DefaultStorage()
                r = requests.get(remote_url, stream=True)
                self.image_preview_status = r.status_code  # save the status code of our attempt
                if r.status_code == 200:
                    name, ext = os.path.splitext(urlparse.urlparse(r.url).path)
                    storage_name = '{upload_to}/cached/{filename}{ext}'.format(
                        upload_to=self.image.field.upload_to,
                        filename=md5(remote_url).hexdigest(),
                        ext=ext)
                    if not store.exists(storage_name):
                        r.raw.decode_content = True
                        store.save(storage_name, r.raw)
                        self.image = storage_name
                self.save()
        return self.image

This mixin is used by LocalIssuer and LocalBadgeClass in apps/composition/models.py.

IssuerSerializer and also BadgeClassSerializer fails
to validate images and possibly allows to upload arbitrary files with
arbitrary extensions:

    def validate_image(self, image):
        # TODO: Make sure it's a PNG (square if possible), and remove any baked-in badge assertion that exists.

Recommendation:

  1. cache images immediately, otherwise they can be changed later,
  2. validate that the images are really imagesm
  3. protect against imagebombs and polyglot files if possible,
  4. make sure the extension matches the image type.

JWT signed badges signatures can be forged

It is possible to forge the JWT token processed by def get_badge_instance_from_jwt()
https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/

Neither the implementation or the pyjwt module blacklist the 'none' algorithm and they both process and trust the alg parameter passed in the JWT token.

Excerpt from the apps/verifier/utils.py:

def get_badge_instance_from_jwt(jwt_string):
    (header, payload, signature,) = jwt_string.split('.')
    algorithm = json.loads(b64decode(header))['alg'].upper()

    badge_instance = json.loads(b64decode(payload))
    if badge_instance['verify']['type'] == 'hosted':
        raise ValidationError(
            "A signed badge instance must be validated via public key not hosted.")
    url = badge_instance['verify']['url']
    public_key = _fetch(url)

    try:
        payload = api_jws.decode(jwt_string, public_key,
                                 algorithm=algorithm)
    except InvalidTokenError:
        raise ValidationError(
            "The signed badge instance did not validate against its public key.")

    badge_instance = badge_instance
    return (None, badge_instance)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.