{
  "variant_id": "pruva-egw-isparticipant-bypass-overlay-ajaxwrite",
  "created_at": "2026-07-07T23:30:00Z",
  "variant_summary": "Bo::isParticipant() authz bypass via request-supplied participants[] array persists on the fixed version (26.2.20260224) through the Overlay::ajax_write() entry point. The fix only hardened SmallPartMediaRecorder::ajax_upload() with an (int) cast; isParticipant() itself was never changed, so any caller passing raw request data to isTeacher()/isParticipant() is still vulnerable.",
  "relation": "newer_version_sibling",
  "origin_kind": "pruva_variant",
  "repository": "github.com/EGroupware/smallpart",
  "submitted_target": {
    "target_kind": "docker_image",
    "commit_sha": "736702baa2c0f5bc7660821436b5ffe947d46d2c",
    "version": "26.2.20260216",
    "ref": "26.2.20260216",
    "display": "egroupware/egroupware:26.2.20260216 (vulnerable)"
  },
  "variant_target": {
    "target_kind": "docker_image",
    "commit_sha": "b8754b6b8ec41d94807e2eecbeded3dc8b7f834a",
    "version": "26.2.20260224",
    "ref": "26.2.20260224",
    "display": "egroupware/egroupware:26.2.20260224 (fixed)"
  },
  "same_root_cause_confidence": "high",
  "same_surface_confidence": "high",
  "claimed_surface": "api_remote",
  "validated_surface": "api_remote",
  "required_entrypoint_kind": "endpoint",
  "required_entrypoint_detail": "POST /egroupware/json.php?menuaction=EGroupware\\SmallParT\\Overlay::ajax_write with JSON body containing course_id as a PHP array with participants[].participant_role=3",
  "attacker_controlled_input": "JSON request body: {\"request\":{\"parameters\":[{\"course_id\":{\"participants\":[{\"account_id\":<attacker_id>,\"participant_role\":3}],\"course_id\":\"1\"},...}]}} — the course_id parameter is a PHP array (JSON object) with a participants key containing the attacker's own account_id and an arbitrary participant_role value",
  "trigger_path": "POST /egroupware/json.php?menuaction=Overlay::ajax_write (nginx->php-fpm) -> Overlay::aclCheck($data['course_id'], true) -> Bo::getInstance()->isParticipant($course_id_array) caches attacker-supplied role -> Bo::getInstance()->isTeacher($course_id_array) returns true via cached role -> aclCheck passes -> Overlay::write() throws InvalidArgumentException (pre-existing type guard blocks array) — authz bypass confirmed, write blocked by incidental type check",
  "observed_impact_class": "authorization_bypass",
  "exploitability_confidence": "high",
  "evidence_scope": "production_path",
  "runtime_manifest_present": true,
  "end_to_end_target_reached": true,
  "inferred": false,
  "blocking_mitigation": "Overlay::write() has a pre-existing is_int/is_numeric type guard (present since before the fix) that rejects array course_id values, preventing the overlay write from completing. However, the authz bypass itself is confirmed — the ACL check is bypassed. Other callers without such type guards (e.g., Bo::save() via Courses::edit()) remain fully exposed.",
  "file_path": "smallpart/src/Bo.php",
  "line_start": 2339,
  "line_end": 2370,
  "secondary_anchors": [
    {
      "file_path": "smallpart/src/Overlay.php",
      "line_start": 816,
      "line_end": 820
    },
    {
      "file_path": "smallpart/src/Widgets/SmallPartMediaRecorder.php",
      "line_start": 19,
      "line_end": 22
    }
  ],
  "review_scope_paths": [
    "smallpart/src/Bo.php",
    "smallpart/src/Overlay.php",
    "smallpart/src/Widgets/SmallPartMediaRecorder.php",
    "smallpart/src/Student/Ui.php",
    "smallpart/src/Courses.php",
    "smallpart/src/Questions.php"
  ],
  "artifact_refs": {
    "variant_manifest": "bundle/vuln_variant/variant_manifest.json",
    "validation_verdict": "bundle/vuln_variant/validation_verdict.json",
    "runtime_manifest": "bundle/vuln_variant/runtime_manifest.json",
    "repro_log": "bundle/logs/vuln_variant_reproduction.log",
    "root_cause_equivalence": "bundle/vuln_variant/root_cause_equivalence.json",
    "reproducer": [
      "bundle/vuln_variant/reproduction_steps.sh"
    ]
  }
}
