#33199 closed New feature (fixed)
Deprecate passing positional arguments to Signer.
Reported by: | Daniel Samuels | Owned by: | Abhinav Yadav |
---|---|---|---|
Component: | Core (Other) | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | Florian Apolloner, Daniel Samuels, Bill Collins, Abhyudai | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
We discovered a vulnerability in one of our applications recently which was caused by an inaccurate instantiation of django.core.signing.Signer
. The developer intended to use the user's email address as the salt for the Signing instance but instead caused it to be used as the key. Here's an example code block that demonstrates the problem:
signer = Signer(self.context['request'].user.email) signed_data = signer.sign_object(dict( license_number='...', product_id='...', device_count='...' ))
In our case, this signed data was then being used to verify a later request and generate an active license. This meant that an attacker could feasibly generate their own licenses if they realised that their email address was the key. The fix for this was to add salt=
in front of the email variable. It occurred to us that this is a relatively easy mistake to make and could be avoided if the signature of Signer.__init__
was changed thusly:
- def __init__(self, key=None, sep=':', salt=None, algorithm=None): + def __init__(self, *, key=None, sep=':', salt=None, algorithm=None):
That is, adding a *
after self
to force the developer to name the parameters.
Change History (18)
comment:1 by , 3 years ago
Cc: | added |
---|---|
Summary: | Consider making Signer parameters keyword-only → Make Signer parameters keyword-only. |
Triage Stage: | Unreviewed → Accepted |
Version: | 3.2 → dev |
comment:2 by , 3 years ago
Cc: | added |
---|
comment:4 by , 3 years ago
Cc: | added |
---|
comment:5 by , 3 years ago
I've opened a PR to address this ticket: https://github.com/django/django/pull/14995
comment:6 by , 3 years ago
Has patch: | set |
---|---|
Owner: | changed from | to
Status: | new → assigned |
comment:7 by , 3 years ago
Patch needs improvement: | set |
---|
comment:8 by , 3 years ago
Summary: | Make Signer parameters keyword-only. → Deprecate passing positional arguments to Signer. |
---|
comment:9 by , 3 years ago
Cc: | added |
---|
comment:10 by , 3 years ago
Owner: | changed from | to
---|
Hi!
It looks like Daniel does not have time to handle this ticket so i would like to have a try.
I spent a little time thinking about the best way to do this and I see a few options:
- Write a decorator that makes the function accept positional arguments, sends a warning and rewrites the call to using kwargs only (Proof of concept)
- Add
*args
to the signature and use a trick withlocals()
to inject those into the local variables (Proof of concept, thanks to t00n for the idea and code) - Change the signature to
__init__(self, *args, **kwargs)
1 and 2 have the advantage of not changing the signature and body of the function too much so it will be easy to remove when the depreciation period ends but are using maybe too much of dark magic. 3 is the opposite.
What do you think would be best in Django's case ? When i know what approach to take, i'd be happy to update Daniel's PR.
comment:11 by , 3 years ago
I think prepending *args
before the existing arguments is good, but if you can avoid tinkering with locals()
, the better, I think.
What about something like:
diff --git a/django/core/signing.py b/django/core/signing.py index 5ee19a9336..724a7df507 100644 --- a/django/core/signing.py +++ b/django/core/signing.py @@ -37,10 +37,12 @@ import base64 import datetime import json import time +import warnings import zlib from django.conf import settings from django.utils.crypto import constant_time_compare, salted_hmac +from django.utils.deprecation import RemovedInDjango50Warning from django.utils.encoding import force_bytes from django.utils.module_loading import import_string from django.utils.regex_helper import _lazy_re_compile @@ -145,16 +147,25 @@ def loads(s, key=None, salt='django.core.signing', serializer=JSONSerializer, ma class Signer: - def __init__(self, key=None, sep=':', salt=None, algorithm=None): + def __init__(self, *args, key=None, sep=':', salt=None, algorithm=None): self.key = key or settings.SECRET_KEY self.sep = sep + self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__) + self.algorithm = algorithm or 'sha256' + if args: + warnings.warn( + 'Providing positional arguments to Signer is deprecated.', + RemovedInDjango50Warning, + stacklevel=2, + ) + for arg, attr in zip(args, ['key', 'sep', 'salt', 'algorithm']): + if arg or attr == 'sep': + setattr(self, attr, arg) if _SEP_UNSAFE.match(self.sep): raise ValueError( 'Unsafe Signer separator: %r (cannot be empty or consist of ' 'only A-z0-9-_=)' % sep, ) - self.salt = salt or '%s.%s' % (self.__class__.__module__, self.__class__.__name__) - self.algorithm = algorithm or 'sha256'
comment:12 by , 3 years ago
Owner: | removed |
---|---|
Status: | assigned → new |
comment:14 by , 2 years ago
Needs documentation: | set |
---|---|
Needs tests: | set |
Patch needs improvement: | set |
comment:15 by , 2 years ago
Needs documentation: | unset |
---|---|
Needs tests: | unset |
Patch needs improvement: | unset |
comment:16 by , 2 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
Agreed. This is a backward incompatible change so release notes and a
versionchanged
annotation is necessary, we also need to audit all documented uses.