Opened 3 years ago

Closed 2 years ago

Last modified 14 months ago

#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 Mariusz Felisiak, 3 years ago

Cc: Florian Apolloner added
Summary: Consider making Signer parameters keyword-onlyMake Signer parameters keyword-only.
Triage Stage: UnreviewedAccepted
Version: 3.2dev

Agreed. This is a backward incompatible change so release notes and a versionchanged annotation is necessary, we also need to audit all documented uses.

comment:2 by Daniel Samuels, 3 years ago

Cc: Daniel Samuels added

comment:3 by Mariusz Felisiak, 3 years ago

Daniel, would you like to prepare a patch?

comment:4 by Bill Collins, 3 years ago

Cc: Bill Collins added

comment:5 by Daniel Samuels, 3 years ago

I've opened a PR to address this ticket: https://github.com/django/django/pull/14995

comment:6 by Jacob Walls, 3 years ago

Has patch: set
Owner: changed from nobody to Daniel Samuels
Status: newassigned

comment:7 by Mariusz Felisiak, 3 years ago

Patch needs improvement: set

comment:8 by Mariusz Felisiak, 3 years ago

Summary: Make Signer parameters keyword-only.Deprecate passing positional arguments to Signer.

comment:9 by Abhyudai, 3 years ago

Cc: Abhyudai added

comment:10 by Nikita Marchant, 3 years ago

Owner: changed from Daniel Samuels to Nikita Marchant

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:

  1. Write a decorator that makes the function accept positional arguments, sends a warning and rewrites the call to using kwargs only (Proof of concept)
  2. Add *args to the signature and use a trick with locals() to inject those into the local variables (Proof of concept, thanks to t00n for the idea and code)
  3. 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 Jacob Walls, 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 Mariusz Felisiak, 3 years ago

Owner: Nikita Marchant removed
Status: assignednew

comment:13 by Abhinav Yadav, 2 years ago

Owner: set to Abhinav Yadav
Patch needs improvement: unset
Status: newassigned

comment:14 by Mariusz Felisiak, 2 years ago

Needs documentation: set
Needs tests: set
Patch needs improvement: set

comment:15 by Abhinav Yadav, 2 years ago

Needs documentation: unset
Needs tests: unset
Patch needs improvement: unset

comment:16 by Mariusz Felisiak, 2 years ago

Triage Stage: AcceptedReady for checkin

comment:17 by Mariusz Felisiak <felisiak.mariusz@…>, 2 years ago

Resolution: fixed
Status: assignedclosed

In b8738aea:

Fixed #33199 -- Deprecated passing positional arguments to Signer/TimestampSigner.

Thanks Jacob Walls for the implementation idea.

comment:18 by Mariusz Felisiak <felisiak.mariusz@…>, 14 months ago

In 3a3e737:

Refs #33199 -- Removed support for passing positional arguments to Signer/TimestampSigner.

Per deprecation timeline.

Note: See TracTickets for help on using tickets.
Back to Top