edubadges / audit Goto Github PK
View Code? Open in Web Editor NEWCode audit repo for Edubadges
Code audit repo for Edubadges
threatLevel="Low" type="User input validation"
The "name" parameter of the award a badge functionality lacks any input validation:
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.
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 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:
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.
A Json Parser error message is shown in the case there is an issue with the Json output.
The web servers for badgr-dev2.edubadges.nl do not respond with an HTTP Strict-Transport-Security header. This means there isn't a Strict Transport Security policy in place.
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.
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.
The application appears to trust the user-supplied host header.
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.
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.
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
.
The badgr-dev2.edubadges.nl webserver supports insecure Medium and RC4 ciphers.
The Openbadge application does not use a secure password policy.
There is no limit on the number of forms a user can submit by (ab)using the "Resend Verification E-mail" functionality.
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.
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.
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:
Recommendation: Remove all unnecessary modules from settings.py
, or only enable them conditionally in a settings_local.py
.
The SSH port (surf-dev2.edubadges.nl) is publicly accessible which increases the attack-vector.
Files are uploaded on the same webserver as the application.
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.
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)
When creating a new issuer only verified e-mail-addresses are listed. However it is possible to intercept the request and change it to a different non-verified e-mail-address.
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.
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.
The API-server (badgr-dev2.edubadges.nl) uses an insecure user session management.
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.
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:
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.
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.
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.
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.
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.
The provider parameter throws a stack-trace-error when using invalid input.
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
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.
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.
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:
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)
When adding a new staff member to an issuer it responses allows to determine if an user exists or not.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.