CVE-2023-48220
Executive Summary
CVE-2023-48220 is a vulnerability in the devise_invitable gem, a dependency of the Decidim participatory democracy framework. The flaw allows an invited user to accept an invitation indefinitely, bypassing the configured invite_for expiry period (set to 2 weeks by default in Decidim). This occurs because the password reset functionality in devise_invitable does not check whether the invitation is still valid before accepting it. An attacker with a valid invitation link can exploit this to gain unauthorized access to a Decidim instance even after the invitation has expired.
The vulnerability affects Decidim versions from 0.0.1.alpha3 up to (but not including) 0.26.9, 0.27.5, and 0.28.0. The root cause lies in the devise_invitable gem, which must be upgraded to version 2.0.9 or later. The CVSS score is 5.7 (Medium), with a low EPSS score of 0.0%, indicating low exploitation activity in the wild. No public exploit code or Nuclei templates are available.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
This vulnerability is a supply chain issue, as it originates from a third-party Ruby gem (devise_invitable). Decidim, being a Ruby on Rails application, relies on this gem for invitation management. The fix requires updating the gem dependency in the Gemfile and running bundle update devise_invitable. Organizations using Decidim should verify their dependency tree and ensure no other gems are affected by similar issues.
CVSS Breakdown & Threat Level
- CVSS Score: 5.7 (Medium)
- Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L
- Attack Vector: Network (AV:N)
- Attack Complexity: Low (AC:L)
- Privileges Required: Low (PR:L) – attacker needs a valid invitation link
- User Interaction: None (UI:N)
- Scope: Unchanged (S:U)
- Confidentiality Impact: Low (C:L)
- Integrity Impact: Low (I:L)
- Availability Impact: Low (A:L)
Threat Level: Medium. The vulnerability requires an invitation link, which limits the attack surface. However, once exploited, an attacker can gain persistent access to a Decidim instance, potentially leading to data manipulation or privilege escalation.
Deep Dive & Mechanics
The devise_invitable gem extends Devise to allow users to be invited to a system. When an invitation is sent, a token is generated and stored in the database. The invitation has an expiry period defined by the invite_for configuration (e.g., 2 weeks). After this period, the invitation should be invalid.
The vulnerability lies in the accept_invitation method, which is called when a user clicks the invitation link. The method checks if the user has been invited but does not verify the invite_for expiry. This means that even if the invitation has expired, the user can still accept it by using the password reset functionality. The password reset flow in Devise calls accept_invitation internally, bypassing the expiry check.
Technical Flow:
- An admin invites a user (e.g.,
user@example.com). - The invitation link is sent with a token.
- The invitation expires after 2 weeks.
- The attacker obtains the invitation link (e.g., via email interception or database access).
- The attacker visits the invitation link and is prompted to set a password.
- Instead of setting a password, the attacker uses the "Forgot your password?" link.
- The password reset flow calls
accept_invitation, which does not check theinvite_forexpiry. - The attacker sets a new password and gains access to the account.
Vulnerable vs. Patched Code
Vulnerable Code (devise_invitable < 2.0.9):
# In lib/devise_invitable/models.rb
def accept_invitation!
self.invitation_accepted_at = Time.current
self.invitation_token = nil
save(validate: false)
end
This method does not check invitation_due_at or invite_for.
Patched Code (devise_invitable >= 2.0.9):
# In lib/devise_invitable/models.rb
def accept_invitation!
if invitation_due_at && invitation_due_at < Time.current
errors.add(:base, :invitation_expired)
return false
end
self.invitation_accepted_at = Time.current
self.invitation_token = nil
save(validate: false)
end
The patched version checks invitation_due_at against the current time and rejects expired invitations.
Exploitation & Lab Setup
Red Team (Exploitation)
Prerequisites:
- A valid invitation link for a Decidim instance (e.g.,
https://decidim.example.com/users/invitation/accept?invitation_token=abc123). - The invitation must have been sent but may be expired.
Steps:
- Obtain the invitation link (e.g., via phishing, email access, or database dump).
- Visit the link in a browser. You will be prompted to set a password.
- Instead of setting a password, navigate to the password reset page (e.g.,
https://decidim.example.com/users/password/new). - Enter the invited user's email address.
- Check the email inbox for the password reset link (if you have access) or use a timing attack to guess the token.
- Click the password reset link and set a new password.
- Log in with the new password. The account is now active.
Proof of Concept (PoC):
# Assuming you have the invitation token
curl -c cookies.txt -b cookies.txt -L "https://decidim.example.com/users/invitation/accept?invitation_token=abc123"
# Then trigger password reset
curl -c cookies.txt -b cookies.txt -X POST "https://decidim.example.com/users/password" \
-d "user[email]=attacker@example.com"
# Follow the password reset link from email
curl -c cookies.txt -b cookies.txt -L "https://decidim.example.com/users/password/edit?reset_password_token=xyz789"
# Set new password
curl -c cookies.txt -b cookies.txt -X PUT "https://decidim.example.com/users/password" \
-d "user[password]=NewP@ss123&user[password_confirmation]=NewP@ss123"
Blue Team (Detection & Prevention)
Detection:
- Monitor for multiple password reset requests for the same user within a short time.
- Alert on invitations accepted after the configured
invite_forperiod. - Log all invitation acceptance events and correlate with password reset events.
Prevention:
- Upgrade
devise_invitableto version 2.0.9 or later. - As a workaround, manually cancel expired invitations in the database:
UPDATE users SET invitation_token = NULL, invitation_accepted_at = NULL WHERE invitation_due_at < NOW(); - Implement additional monitoring on the
userstable for unexpected changes toinvitation_accepted_at.
Indicators of Compromise (IOCs)
- Network: Unusual password reset requests for invited users.
- Logs: Multiple
Devise::PasswordsController#createactions followed byDevise::InvitationsController#editfor the same user. - Database: Rows in
userstable whereinvitation_accepted_atis set butinvitation_due_atis in the past. - File: Modified
Gemfile.lockshowingdevise_invitableversion < 2.0.9.
Detection Engineering
YARA Rule
rule Detect_Devise_Invitable_Vulnerability {
meta:
description = "Detects vulnerable devise_invitable gem versions in Gemfile.lock"
author = "Valters IT"
date = "2025-04-10"
cve = "CVE-2023-48220"
strings:
$gem = "devise_invitable"
$version_low = /devise_invitable\s*\(([0-1]\.\d+\.\d+|2\.0\.[0-8])\)/
condition:
$gem and $version_low
}
Sigma Rule
title: CVE-2023-48220 - Invitation Expiry Bypass via Password Reset
id: 7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d
status: experimental
description: Detects password reset requests for invited users that may indicate exploitation of CVE-2023-48220
author: Valters IT
date: 2025-04-10
logsource:
category: application
product: decidim
detection:
selection:
event: "password_reset_request"
user_invited: true
condition: selection
falsepositives:
- Legitimate password resets for invited users
level: medium
Suricata Rule
alert http any any -> any any (msg:"CVE-2023-48220 - Invitation Bypass via Password Reset"; flow:to_server; content:"/users/password"; http_uri; content:"user[email]"; http_client_body; classtype:attempted-user; sid:1000001; rev:1;)
Remediation & Mitigation
Immediate Actions:
- Upgrade
devise_invitablegem to version 2.0.9 or later in theGemfile:gem 'devise_invitable', '~> 2.0.9' - Run
bundle update devise_invitableand restart the application. - If upgrade is not possible, manually cancel all expired invitations in the database:
UPDATE users SET invitation_token = NULL, invitation_accepted_at = NULL WHERE invitation_due_at < NOW();
Long-term Measures:
- Implement automated dependency scanning (e.g., Dependabot, Snyk) to detect vulnerable gems.
- Enforce invitation expiry checks at the application level as a defense-in-depth measure.
- Monitor for unusual invitation acceptance patterns.
Workaround:
- Cancel invitations directly from the database as described above. This is a temporary measure until the gem is upgraded.
Valters IT
Senior DevSecOps & Cyber Threat Intelligence Expert