#33199 closed New feature (fixed)
Deprecate passing positional arguments to Signer.
| Reported by: | Daniel Finch | Owned by: | Abhinav Yadav |
|---|---|---|---|
| Component: | Core (Other) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Florian Apolloner, Daniel Finch, 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 , 4 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 , 4 years ago
| Cc: | added |
|---|
comment:4 by , 4 years ago
| Cc: | added |
|---|
comment:5 by , 4 years ago
I've opened a PR to address this ticket: https://github.com/django/django/pull/14995
comment:6 by , 4 years ago
| Has patch: | set |
|---|---|
| Owner: | changed from to |
| Status: | new → assigned |
comment:7 by , 4 years ago
| Patch needs improvement: | set |
|---|
comment:8 by , 4 years ago
| Summary: | Make Signer parameters keyword-only. → Deprecate passing positional arguments to Signer. |
|---|
comment:9 by , 4 years ago
| Cc: | added |
|---|
comment:10 by , 4 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
*argsto 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 , 4 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 , 4 years ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:14 by , 3 years ago
| Needs documentation: | set |
|---|---|
| Needs tests: | set |
| Patch needs improvement: | set |
comment:15 by , 3 years ago
| Needs documentation: | unset |
|---|---|
| Needs tests: | unset |
| Patch needs improvement: | unset |
comment:16 by , 3 years ago
| Triage Stage: | Accepted → Ready for checkin |
|---|
Agreed. This is a backward incompatible change so release notes and a
versionchangedannotation is necessary, we also need to audit all documented uses.