2.47.0.txt

doc/release_notes/2.47.0.txt

Security Fixes to Address Reported Vulnerabilities

There were a large number of reported vulnerabilities that are fixed in this version. While none are as critical as the webauthn_login vulnerability fixed in 2.46.0, this release does fix some serious vulnerabilities. The vulnerabilities are separated by type:

  • Security Issues Not Requiring Race Conditions or Privileged Information

  • Security Issues Not Requiring Race Conditions

  • Race Conditions Not Requiring Privileged Information

  • Race Conditions Requiring Privileged Information

Thank you to Joshua Rogers (joshua.hu) of AISLE Research (aisle.com) for privately reporting these vulnerabilities.

Timeline (all times UTC):

  • 2026-08-22 10:25am: Email received from Joshua Rogers with 12 security findings.

  • 2026-08-22 19:37pm: Email from Joshua Rogers opened, analysis started.

  • 2026-08-22 20:33pm: Response to Joshua Rogers confirming the findings and initial analysis.

  • 2026-08-22 22:23pm: Email received from Joshua Rogers with 14 additional security findings.

  • 2026-08-23 00:54am: Response to Joshua Rogers confirming the findings and initial analysis of second report.

  • 2026-08-24 01:39am: Fixes developed and committed, work begins on the 2.47.0 release.

  • 2026-08-24 05:20am (approximate): Rodauth 2.47.0 released

Security Issues Not Requiring Race Conditions or Privileged Information

  • The return to path configuration methods would previously allow redirecting to remote URLs, most simply by making a request to a path that started with //. The related configuration methods (both of which return false by default) are:

    • login_return_to_requested_location?

    • two_factor_auth_return_to_requested_location?

    The confirm_password feature implicit works by returning to requested locations, so it was also vulnerable.

    All of these now check that the return to path starts with / but not //, ensuring that it should be considered a local path and not a remote URL.

    Additionally, only the login_return_to_requested_location? feature implemented checks for request method (only allow if original request used GET) and path size (only allow if under 2K bytes). These checks have been moved from the login feature to the base feature, and are now applied to all three cases. Additionally, two configuration methods are added to configure this support:

    • return_to_path_max_size: The maximum size in bytes of a path to return to.

    • valid_return_to_path?(path): This determines whether the path is valid. By default it does the / but not // check and the path size check.

    This change can break applications that are designed to redirect to remote URLs, such as applications that operate as an authentication gateway for other domains. Such applications should use the valid_return_to_path? configuration method appropriately for their needs.

  • The json feature previously used a content type regexp that was not anchored to the beginning of the content-type header. This allowed using application/json as a type parameter to another type that was allowed without a CORS preflight request. As Rodauth allows JSON requests without CSRF by default (thinking that they would be require a CORS preflight request and therefore the CSRF is unnecessary), this resulted in Rodauth being open to CSRF attacks in some cases if the json feature was enabled, the check_csrf was not used explicitly to require CSRF checks, and the json_request_content_type_regexp was not used explicitly with a properly anchoring regexp.

    Whether the application is actually vulnerable to CSRF depends on the session implementation. With Roda’s sessions plugin, and other session implementations that use SameSite=Lax, CSRF attacks are limited to same-site-cross-origin attacks. For endpoints that don’t require a logged in session, such as the login endpoint, the SameSite protection doesn’t apply, and this could be used to force login into an attacker-controlled account.

    This issue is fixed by anchoring the default json_request_content_type_regexp with A, preventing the ability to have application/json as a type parameter be considered a json request. Cross origin requests that Rodauth would treat as JSON requests now require a CORS preflight request.

  • Rodauth’s documentation was updated to point out potential session fixation problems when using session implementations based on Rack::Session::Abstract::Persisted. If you are using a session implementation based on Rack::Session::Abstract::Persisted, and cannot switch to another approach, you should use the clear_session configuration method to force a new session:

    clear_session do
      session.options[:renew] = true
      session.clear
    end
    

Security Issues Not Requiring Race Conditions

  • The jwt feature now removes the Authorization response header if the request is not a POST request or it doesn’t accept JSON. When using the jwt_refresh feature, this fixes the ability to get a new access token by submitting a non-POST request to the jwt_refresh route.

    Additionally, the jwt_refresh feature no longer allows decoding expired JWTs for non-POST requests. Previously, decoding of expired JWTs was allowed during requests to the jwt_refresh path regardless of the request method used.

    Previously, this issue could be abused to continually get new access tokens if an attacker already had a expired access token but no refresh token.

  • The otp feature now prevents otp code reuse during the drift window. Previously, it was possible to replay an otp code during the drift window. In order for this to be exploitable, the attacker must have access to an already used but otherwise valid OTP code. While not technically a race condition, the window in which this could be exploited would be limited to 30 seconds by default. Note that preventing reuse during the drift window requires rotp 4 (released in March 2018).

  • Adding a webauthn authenticator after using the webauthn login feature no longer marks the session as being two factor authenticated. In order for this to be exploitable, the application would need to restrict some endpoints to only sessions with two factor authentication. If a user wanted to exploit this, they could easily setup an actual separate factor, access what they want, and then remove the separate factor, so this bug in general allowed security policy violations but not actual security vulnerabilities.

    Related to this bug fix, attempts to update a session to being two factor authenticated by using an authentication type that was used as the original factor will now raise a Rodauth::Error.

  • A jwt_refresh token created using the old hmac secret instead of the current hmac secret was previously not deleted from the database during logout, allowing it to be used until it expired for creating new refresh tokens. This is only exploitable if the attacker gains access to a refresh token created using the old hmac secret after it was used during logout, and only then until the refresh token expires.

  • When using the password_pepper feature and using the password_maximum_bytes configuration method to set a maximum size for the password, the length of the password pepper was not included in the calculation, allowing the actual password given to the hash algorithm to be longer than password_maximum_bytes. When using the bcrypt hash algorithm with a password_maximum_bytes of 72 (the bcyrpt limit), this could allow long bcrypt passwords to be set in such as way as that some or all of the pepper bytes are ignored. In general, a 72 byte bcrypt password is probably secure enough the pepper adds no significant additional security, but it is possible there are cases where the pepper would be needed for security, and the user should provide a shorter bcrypt password to accomodate the pepper. In order to exploit this feature (use a password that would be truncated by bcrypt), an attacker must have access to either change the password or login with the password.

    Note that Rodauth does not set password_maximum_bytes by default, but in Rodauth 3, password_maximum_bytes will default to 72 if bcrypt is used as the password hash algorithm.

  • Non-json requests to the POST endpoints for the email token features will no longer consider a token submitted as a parameter. These features are:

    These features all have the same expected non-JSON workflow:

    • Submit GET request with token parameter (click on link in email)

    • GET request takes token parameter, validates it, adds it to session, redirects to same path with no parameter

    • With token in session, GET request shows form for completing action

    • Form submits via POST

    In these workflows, the token should never been provided as a parameter to the POST request. For non-json requests, the POST endpoints will no longer consider tokens submitted as a parameter, protecting them somewhat against cases where the application would be vulnerable to CSRF attacks.

    In order to exploit this issue, an attacker must have access to a current valid token, and the application must be vulnerable to CSRF.

    It’s possible this change could break custom workflows. The allow_param_fallback_for_session_param? configuration method has been added, which can be set to true if you need to support tokens as parameters to POST endpoints in non-JSON workflows.

  • When using the webauthn_login feature and a passwordless webauthn login, adding or removing a webauthn authenticator is no longer allowed if the account is setup for two factor authentication but the session is not two factor authenticated. Previously, you could add or remove a webauthn authenticator in this case, but that goes against Rodauth’s general principle of requiring two factor authentication for account changes if the account is setup for two factor authentication. In order to exploit this, an attacker would need access to a valid webauthn authenticator or a valid session for a passwordless account that has been setup for two factor authentication.

Race Conditions Not Requiring Privileged Information

  • The otp and sms_codes features both allowed concurrent authentication requests for the same account to bypass the limit on allowed failures, allowing them more guesses than would otherwise be allowed. The features now use transactions and FOR UPDATE queries to enforce specified limits.

    In general, while this can increase the risk of an account compromise, the maximum number of concurrent requests is still low enough to make a blind guess approach successful. Trying to exploit this is also very noisy, and an unsuccesful attack will result in otp or sms lockout. Additionally, attackers can only attempt this attack after they have already partially compromised the acccount (such as getting single factor access but not multifactor access). This is only a feasible exploit approach if you have limited the number of potential values to more than the number the allowed failures but less than number of concurrent failing requests before triggering lockout.

Race Conditions Requiring Privileged Information

  • When setting a new password, handle cases where there was a concurrent password change by reporting an error, instead of silently succeeding. This issue is only exploitable if an attacker has the ability to change the password.

    set_password now raises a Rodauth::SetPasswordFailure exception in this case, and callers now handle this exception:

    Note that the change password security requires that the user provide the existing password. If password modifications do not require the current password provided, than the change cannot be protected against.

  • All features that used email tokens were subject to race conditions for concurrent requests using those tokens. These features are:

    In order to be exploitable, an attacker must have access to the related token, and must choose to use them during a concurrent request by the victim.

    These issues were all addressed by expanding transaction scope and using FOR UPDATE on the query to retrieve the token to serialize the access.

    In addition to this fix, these features leaked tokens into the session in some cases. The features have been updated to delete the tokens appropriately from the session.

  • The recovery_codes feature was vulnerable to concurrent use of the same recovery code. When removing the recovery code during the recovery code authentication process, Rodauth now checks that a single row was deleted, and returns false if that isn’t the case. This issue was only exploitable if an attacker had access to a valid recovery code, and chose to authenticate with it during a concurrent request by the victim with the same code.

  • The jwt_refresh feature was vulnerable to concurrent use of the refresh token. It was also fixed by expanding transaction scope and using FOR UPDATE. This issue was only exploitable if an attacker had access to the same refresh token as the victim, and chose to refresh the token during a concurrent request by the victim.

  • The verify_account feature was updated to only verify the account if the account was still in an unverified state during the query to verify it. This could only be exploited if an attacker had a valid verify account token, and would require the account have a concurrent account status change. Potentially, the previous behavior could allow a concurrent account close during the grace period (with the verify_account_grace_period feature) to be overridden by the account verification.

Other Security Fixes

  • The single_session feature now treats a session as invalid if the session does not have a single session key, but a single session key for the account exists in the database. This indicates that the single_session feature was added after the session was created, and that there was another session created afterward (for the database to have a session). In this case, it appears there is another, more recent session, so the current session should be considered invalid.

  • The password_grace_period now prevents extending the grace period by using the change password feature and attempting to change the password to the current password.

Other Improvements

  • Cases where Rodauth could use either the account_id or the session value have been consolidated to use an approach where the account_id is used if there was an attempt to load an account, and the session_value is used only if there was no attempt to load an account. That is safer the approach of using the session_value if the account_id is not available (even if there was an attempt to load an account), or worse, using the session_value if available instead of the account_id (the cause of the webauthn_login vulnerability fixed in 2.46.0). There were two cases where the session_value was used in preference to the account_id:

    • webauthn_verify_account: Unlikely webauthn_login, this doesn’t appear to have been exploitable unless you have the verify account token, in which case you don’t need an exploit to control the account.

    • active_sessions: This could potentially be used to remove, clear, or set active session records for an attacker controlled account. It does not allow an attacker to remove, clear, or set active session records for an account they do not control. Since users often have the ability to remove, clear, or set their own active session records, this does not appear to have been exploitable.

  • Rodauth now warns or raises if you attempt to load an account multiple times in the same instance during a Rodauth route. Rodauth now records the reason the account was loaded. If an account was loaded multiple times for the same reason (e.g. both the from session value), Rodauth warns, since this is inefficient but unlikely to be a security issue. If an account was loaded for a different reason than the original load, Rodauth raises a Rodauth::Error, as this can result in a security issue due to potential account confusion.

    This found a couple of duplicate retrieval issues inside Rodauth, which this release fixes.

    If this account reloading prevention causes problems in your application, is probably better to fix the underlying issues, but you can use the new account_reload configuration method to allow account reloads by having it return nil and not call super.

  • The security fix in 2.46.0 could result in require_authentication failing in applications using the webauthn_login feature. The new approach of using the account_id if an account is loaded, instead of basing the account_id/session_value decision on the current route, should fix this issue.

  • The jwt_cors feature now sets the Vary response header to Origin so that caches will not return a response for one origin if there is a request for different origin. As Rodauth’s JSON API only supports POST requests, it is unlikely a response would be cached, so this is done as a best practice and not to prevent an actual vulnerability. A jwt_cors_vary_header configuration method has been added to use to configure the value of the Vary header.

  • The verify_login_change feature now disallows verification for unverified or closed accounts. Unverified accounts should verify themselves before changing their login, and there is no reason to change the login for a closed account.

  • The README received substantial updates to explain how Rodauth handles some security aspects, as well as recommended configuration methods to use to improve security.

Backwards Compatibility

  • Rodauth now warns if you are loading a feature that should be using a configuration method for security reasons, but the configuration method is not used. The following configuration methods have such warnings:

    • domain: By default, this uses request.host, which comes from Rack, and is insecure by default because it tries to determine the host from the request using potentially attacker-provided information. In any case where you will be sending emails, not setting domain is likely to result in a serious security issue in the application.

    • hmac_secret: Rodauth has significant security benefits from setting an hmac secret for the features that can use it but do not require it.

    • already_logged_in: For backwards compatibility, Rodauth allows already authenticated sessions to access endpoints that are designed to be used by unauthentication sessions. This hightens the risk of account confusion attacks. It’s strongly recommended that this configuration method be used to halt or redirect, in order to prevent potential account confusion attacks (doing so prevented the webauthn_login vulnerability fixed in 2.46.0).

    If you are not loading a feature that uses the configuration method, Rodauth will not emit a warning for the method. If you want to avoid a particular warning and use the default value, you can call the configuration method with a block containing super(). Alternatively, to change the behavior for all of these configuration warnings, you can use the new missing_recommended_configuration(msg) configuration method.

    In Rodauth 3, these missing recommended configuration warnings will change to raised exceptions by default.

  • External features that set the @account instance variable should switch to using the new set_account internal method. If the feature is designed to reload accounts, it can set @account_retrieval_type to nil before calling set_account.