Root cause
Validation & fail-open logic
feat: add external REST API (v1 & v2) with token auth and in-app docs
How AI contributed: New attack surfacePublished Jul 20, 2026 in
rconfig/rconfig
rConfig Core before 8.2.8 contains a privilege escalation vulnerability that allows authenticated users to assign arbitrary roles to any account by submitting an unvalidated role field through the Users API during user creation or profile updates. Attackers can exploit the missing allowlist validation and absent admin-level authorization check in StoreUserRequest to mass-assign the Admin role directly to the User model, granting access to privileged features. rConfig Pro and Enterprise are not affected.
AI-written code in StoreUserRequest failed to validate the role field and skipped admin authorization, letting any authenticated user mass-assign the Admin role and escalate privileges.
Root cause
feat: add external REST API (v1 & v2) with token auth and in-app docs
Fix
Restrict role field to a defined allowlist on user create and update, and require the acting user already hold the Admin role before granting it to another account. Adds regression tests covering role validation and privilege checks.
Fix by Stephen Stack · no AI marker found
Case facts
--- /dev/null+++ b/app/Http/Controllers/Api/SettingRestApiTokenController.php@@ -0,0 +1,66 @@+<?php++namespace App\Http\Controllers\Api;++use App\Http\Controllers\Controller;+use App\Http\Requests\StoreRestApiTokenRequest;+use App\Models\RestApiToken;+use App\Traits\RespondsWithHttpStatus;+use Illuminate\Http\JsonResponse;+use Illuminate\Support\Str;++class SettingRestApiTokenController extends Controller+{+ use RespondsWithHttpStatus;++ /**+ * List existing tokens. The raw token value is never returned here; it is only+ * shown once at creation time.+ */+ public function index(): JsonResponse+ {+ $tokens = RestApiToken::query()+ ->select(['id', 'api_token_name', 'api_token_status', 'created_at', 'updated_at'])+ ->orderByDesc('created_at')+ ->get();++ return $this->successResponse('Success', $tokens);+ }++ /**+ * Create a new token and return the plaintext value once.+ */+ public function store(StoreRestApiTokenRequest $request): JsonResponse+ {+ $token = (string) Str::uuid();++ $model = RestApiToken::create([+ 'api_token' => $token,+ 'api_token_name' => $request->validated('api_token_name'),+ 'api_token_status' => 1,+ ]);++ return $this->successResponse('API token created successfully!', [+ 'id' => $model->id,+ 'api_token_name' => $model->api_token_name,+ 'api_token' => $token,+ 'created_at' => $model->created_at,+ ]);+ }++ /**+ * Revoke a token.+ */+ public function destroy(int $id): JsonResponse+ {+ $model = RestApiToken::find($id);++ if (! $model) {+ return $this->failureResponse('API token not found.', 404);+ }++ $model->delete();++ return $this->successResponse('API token deleted successfully!', $id);+ }+}--- /dev/null+++ b/app/Http/Controllers/Api/v1/ApiTestController.php@@ -0,0 +1,24 @@+<?php++namespace App\Http\Controllers\Api\v1;++use App\Http\Controllers\Controller;+use Illuminate\Http\JsonResponse;++/**+ * @group API Test+ *+ * @authenticated+ *+ * Simple connectivity check for the v1 REST API.+ */+class ApiTestController extends Controller+{+ public function index(): JsonResponse+ {+ return response()->json([+ 'success' => true,+ 'message' => 'Welcome to the rConfig REST API version 1',+ ], 200);+ }+}--- /dev/null+++ b/app/Http/Controllers/Api/v1/CategoryController.php@@ -0,0 +1,15 @@+<?php++namespace App\Http\Controllers\Api\v1;++use App\Http\Controllers\Api\CategoryController as BaseCategoryController;++/**+ * @group Categories+ *+ * @authenticated+ */+class CategoryController extends BaseCategoryController+{+ // Inherits index/show/store/update/destroy from the application category controller.+}--- a/app/Http/Requests/StoreUserRequest.php+++ b/app/Http/Requests/StoreUserRequest.php@@ -10,11 +10,25 @@ class StoreUserRequest extends FormRequest /** * Determine if the user is authorized to make this request. *+ * Only an existing Admin may set or change a user's role to Admin, whether+ * creating a new user, updating another user, or updating their own account.+ * Any other requested role is left to the validation rules below.+ * * @return bool */ public function authorize() {- return auth()->check(); // returning true if user is logged in+ $actingUser = $this->user();++ if (! $actingUser) {+ return false;+ }++ if ($this->input('role') !== 'Admin') {+ return true;+ }++ return in_array($actingUser->role, ['Admin', 'admin'], true); } /**@@ -32,7 +46,7 @@ class StoreUserRequest extends FormRequest 'email' => 'required|email|unique:users|max:255', 'password' => 'required|min:8', 'repeat_password' => 'required|min:8',- 'role' => 'required',+ 'role' => 'required|in:Admin,User', ]; } @@ -42,7 +56,7 @@ class StoreUserRequest extends FormRequest 'email' => 'required|email|max:255', 'password' => 'required|min:8', 'repeat_password' => 'required|min:8',- 'role' => 'required',+ 'role' => 'required|in:Admin,User', ]; } --- /dev/null+++ b/tests/Fasttests/ControllersTests/Api/UserRoleValidationTest.php@@ -0,0 +1,135 @@+<?php++namespace Tests\Fasttests\ControllersTests\Api;++use App\Http\Requests\StoreUserRequest;+use App\Models\User;+use Tests\TestCase;++/**+ * Regression tests for role field validation and privilege checks on user+ * create/update.+ *+ * Enforcement under test:+ * - app/Http/Requests/StoreUserRequest.php: `role` is validated against a+ * fixed allowlist, and `authorize()` requires the acting user to already+ * hold the Admin role before it can be granted to any account, their own+ * or another's.+ */+class UserRoleValidationTest extends TestCase+{+ protected User $nonAdmin;++ public function setUp(): void+ {+ parent::setUp();+ $this->beginTransaction();+ $this->nonAdmin = User::factory()->create(['role' => 'User']);+ $this->actingAs($this->nonAdmin);+ }++ protected function tearDown(): void+ {+ $this->rollBackTransaction();+ parent::tearDown();+ }++ /**+ * Claim: no allowlist on `role` + no privilege check on store() lets any+ * authenticated user mint a brand new Admin account.+ */+ public function test_creating_a_user_with_admin_role_requires_existing_admin_privilege(): void+ {+ $response = $this->postJson('/api/users', [+ 'name' => 'Attacker Created Admin',+ 'email' => '[email protected]',+ 'username' => 'pwnedadmin',+ 'password' => 'password1',+ 'repeat_password' => 'password1',+ 'role' => 'Admin',+ ]);++ $response->assertStatus(403);++ $this->assertDatabaseMissing('users', [+ 'email' => '[email protected]',+ 'role' => 'Admin',+ ]);+ }++ /**+ * Claim: the same missing check applies to update(), letting a user+ * change their own role instead of creating a new account.+ */+ public function test_updating_own_role_to_admin_requires_existing_admin_privilege(): void+ {+ $response = $this->patchJson('/api/users/' . $this->nonAdmin->id, [+ 'name' => $this->nonAdmin->name,+ 'email' => $this->nonAdmin->email,+ 'password' => 'password1',+ 'repeat_password' => 'password1',+ 'role' => 'Admin',+ ]);++ $response->assertStatus(403);++ $this->assertDatabaseHas('users', [+ 'id' => $this->nonAdmin->id,+ 'role' => 'User',+ ]);+ }++ /**+ * Claim: StoreUserRequest::authorize() must check privilege, not just+ * whether the caller is logged in.+ */+ public function test_store_user_request_authorize_checks_privilege_for_admin_role(): void+ {+ $request = StoreUserRequest::create('/api/users', 'POST', [+ 'role' => 'Admin',+ ]);+ $request->setUserResolver(fn () => $this->nonAdmin);++ $this->assertFalse(+ $request->authorize(),+ 'StoreUserRequest::authorize() returned true for a non-admin user attempting to set role=Admin.'+ );+ }++ /**+ * Claim: the `role` validation rule must reject values outside the+ * known set, not just require the field to be present.+ */+ public function test_role_field_only_accepts_known_values(): void+ {+ $response = $this->postJson('/api/users', [+ 'name' => 'Garbage Role User',+ 'email' => '[email protected]',+ 'username' => 'garbagerole',+ 'password' => 'password1',+ 'repeat_password' => 'password1',+ 'role' => 'TotallyBogusRoleThatDoesNotExist12345',+ ]);++ $response->assertStatus(422);+ $this->assertArrayHasKey('role', $response->json('errors') ?? []);+ }+AI-assisted change 4dfe441cdae40351449848bd9df882cab9d8407f36ac91b809dc928d06bbdce3 · Fix 01da57f867b4dadf886138d86b48ae4128dbb6fd5f851ce2ce1d49185adfccad