Skip to content

Conversation

grantfitzsimmons
Copy link
Member

@grantfitzsimmons grantfitzsimmons commented Sep 9, 2025

Fixes #7398

Checklist

  • Self-review the PR after opening it to make sure the changes look good and
    self-explanatory (or properly documented)
  • Add relevant issue to release milestone

Testing instructions

https://discourse.specifysoftware.org/t/allow-support-login-documentation/2838/1

In your .env file, add the following:

ALLOW_SUPPORT_LOGIN=true
SUPPORT_LOGIN_TTL = 300

Usage steps:

  1. Enter the running Specify 7 container:
    docker exec -it specify7 /bin/bash
  2. Run the support login command:
    ve/bin/python manage.py support_login --username ${username}
    Replace ${username} with the desired account.
  3. The generated token is valid for a time set by the SUPPORT_LOGIN_TTL environment variable (e.g., 300 seconds).
    Example output:
    The following token is valid for 300 seconds:
    /accounts/support_login/?token=1-1757106120-7394fa7d5ffc6f87fe8306d25c5b2c71b3f98942d9f9c46aea97f3eda725434b
  4. Navigate to base_url + /accounts/support_login/?token=... to access the user's account.

@CarolineDenis CarolineDenis added this to the 7.11.2 milestone Sep 9, 2025
Copy link
Contributor

@melton-jason melton-jason left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently1, the security of this approach heavily relies on the SECRET_KEY to be kept a secret.
If the SECRET_KEY is leaked, a malicious user could very easily compromise any user account on the system at any time without having to use the python manage.py support_login command.

In the internal Specify Vault, I've put a video demonstrating exploiting this vulnerability: https://drive.google.com/file/d/1P7zpPiUAxYQnSvhzfXn4UyJ3UkJPpi3h/view?usp=sharing

I know the SECRET_KEY should be kept secret, but this presents a major risk to institutions.

There might be other ways to accomplish this, but I think we can make this more secure by adding a psuedo one-time salt/pad argument to the manage.py support_login command (or we can generate a key for the user at the time they run the command).
We can then hash the token with this generated or provided key.

The user-provided or generated key would then need to be provided when logging in via the /accounts/support_login endpoint.

That way, a malicious agent would require both the SECRET_KEY and the key generated (or provided) when running the support_login command to accomplish the exploit.

I can make the changes to this PR if needed.


On a couple much smaller notes:

Can we remove the following line? 😅
Or add a comment linking to documentation.

# TODO: is this in use somewhere?

Also, there's another option that's currently accepted by the manage.py support_login command: --list!

parser.add_argument(
'--list',
action='store_true',
dest='list',
default=False,
help='List users in database',
)

It lists out all of the users, their user types, and whether they are a Specify 6 admin or not:

Image

The output isn't very clean imo, and there's no way to view Specify 7 admins, so I'd be down to remove the option or clean it up and extend it!

Footnotes

  1. This is not related to the changes in this PR, but with how the feature was initially implemented

@github-project-automation github-project-automation bot moved this from 📋Back Log to Dev Attention Needed in General Tester Board Sep 9, 2025
@CarolineDenis CarolineDenis modified the milestones: 7.11.2, 7.12.0 Sep 11, 2025
@melton-jason melton-jason self-requested a review October 11, 2025 03:17
@melton-jason
Copy link
Contributor

melton-jason commented Oct 16, 2025

This is ready for review and testing!

I've made a number of improvements behind the scenes and have hopefully made this feature a little more secure.

Redis

Primarily we are utilizing redis as an in-memory cache to store information required to process a support login attempt.
We can utilize Redis in the future for other tasks. Its popular use-cases that Specify might be able to leverage (for general use-cases/features, see https://redis.io/solutions/):

More Specify-specific applications might include:

Cryptography

Put simply, the support login process is now as follows:

Currently, a symmetric encryption algorithm is used (AES-GCM with a randomly generated 256 bit key and 256 bit salt) to handle encryption and decryption.
A JSON Web Token (JWT) is used to transmit the ciphertext and publicly available encryption artifacts required for verification and decryption (nonce and MAC tag).

Token Generation/Encryption:

  • The support_login command is hit with the username for someUser
  • Two randomly generated cryptographically-secure 32 byte (256 bit) numbers are generated (one a "private" key to be supplied with the token, and the other a "salt" value)
  • The key and salt are hashed with the SECRET_KEY of the server and passed through a Key Derivation Function to generate two more cryptographically-secure 32 byte numbers: one a "encryption" key and the other a "signing" key
  • The current timestamp of the server is recorded, and the timestamp and details about the user are encrypted
  • The resulting ciphertext and encryption artifacts are encoded into a JWT and supplied to the user along with one of the first two generated numbers
  • The other of the two first generated numbers is stored into the redis cache

Token Parsing/Decryption:

  • (If any of the below fails, the login attempt will be unsuccessful)
  • The backend receives the provided token and key from the GET request
  • Using the key, Specify fetches the other stored key in the redis cache and uses that to regenerate the encryption and signing key
    • Once the key is fetched in this way - or after the SUPPORT_LOGIN_TTL expires - redis will delete the stored key
  • Specify decodes and verifies the JWT
  • Using the payload of the JWT, Specify decrypts the ciphertext
  • Specify verifies the user information and recorded timestamp and, if successful, logs the user in

Let me know if you have any ideas about this current implementation and setup! I'm considering using an asymmetric algorithm to handle encryption/decrpytion (the current implementation kind of follows the asymmetric approach anyways), although finding a way to secure a generated private key is notably difficult (and it may not be worth the potential performance hit).

Further Improvements

If we wish to make this more secure, I would suggest prompting the user for a one-time password - or generate a password for the user and write it to stdout - which will act as the key, and would be used for encryption when they are generating a token.
The /accounts/support_login/ page would then prompt for the aforementioned password, and use a PUT/POST request to login (which will use Django's CSRF protection: something that is lacking in a GET request).

The difference with the current implementation is that the private keys used for cryptography and verification will be strictly separated from the structure of the endpoint: they will only exist while the required message is being encrypted / decrypted after having been obtained by the user.
(The same principal that password-based authentication uses!).


Previously, a malicious agent really only needed to know the SECRET_KEY to compromise any SpecifyUser account.

Now, the goal is that each of the following must hold true for a malicious agent to compromise a SpecifyUser account:

  • The attacker knows the server's SECRET_KEY
  • The attacker has access to write to the redis cache OR has read access to the redis cache within SUPPORT_LOGIN_TTL seconds after a support login token has been generated and before the token has been used
    • With the default docker configuration, only containers in the compose network can connect to the Redis Cache
    • If needed, we can make the default redis configuration more secure by modifying the configuration such that it only accepts connections from the main Specify 7 instance, requires a password, etc.
  • The attacker knows the SpecifyUser -> name accompanied with the SpecifyUser -> id they are trying to compromise
  • The attacker knows the time of the server and and the SUPPORT_LOGIN_TTL

As such, a system administrator should generally follow the following steps to secure their instance:

  • Change the default SECRET_KEY to a randomly generated cryptographically secure SECRET_KEY upon Specify installation
    • Rotate the SECRET_KEY (and other private keys of the instance) reasonably regularly
  • Secure the Specify 7 docker network
    • Ensure no extraneous ports are exposed to the host machine and the wider internet
    • (If possible) reduce the attack surface by using fine-grained permission systems and putting access-control policies in place (require credentials to connect to the redis database, binding access to specific interfaces, etc.)
  • Maintain a small SUPPORT_LOGIN_TTL (the default is 180 seconds)
  • Monitor the instance, database, and redis logs and redis cache

@grantfitzsimmons grantfitzsimmons requested a review from a team October 16, 2025 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Dev Attention Needed

Development

Successfully merging this pull request may close these issues.

Support ALLOW_SUPPORT_LOGIN in Docker

4 participants