Skip to content

Secure the inline column update routes - #18

Merged
fadymondy merged 2 commits into
masterfrom
fix/column-security
Sep 15, 2026
Merged

fadymondy merged 2 commits into
masterfrom
fix/column-security

Conversation

@fadymondy

Copy link
Copy Markdown
Contributor

Vulnerability

PanelServiceProvider registered two inline column update routes (used by ToggleColumn in tables and relation managers, and by the card-grid ToggleGridColumn):

  • PATCH {slug}/{id}/column loaded the record with findOrFail(), which skips tenant scoping. It wrote to any column in the table config, including plain TextColumns. It never checked update authorization and never validated the value.
  • PATCH {slug}/{id}/relations/{relationship}/{relationId}/column wrote request('column') => request('value') onto the related record with no check at all.

As a result, any authenticated panel user could change arbitrary attributes (e.g. is_admin, foreign keys) of records they are not allowed to edit, including records of other tenants.

Fix

Both closures now delegate to a new Laravilt\Panel\Http\Controllers\ColumnStateController. It mirrors the new laravilt/tables column endpoint (tables#11):

  • Allowlist: the column must be in the resource's table() (or the relation manager's table()). It must be editable (EditableColumn), not disabled, and not a relationship path. Non-editable columns get 403; unknown columns get 404; a missing name gets 422.
  • Tenant scoping: the record (or owner record) is loaded through Resource::getEloquentQuery(). Related records are loaded through the relation manager's relationship query, so they must belong to that owner.
  • Authorization: the resource's canUpdate($record) (permissions, or the policy when $usePolicies is set). For the relation route, the relation manager's canEdit() must pass, the owner resource's canUpdate($owner) must pass, and the related model's update policy must pass if one is registered.
  • Validation: the column's getStateValidationRules() (boolean for Toggle/Checkbox, allowed options for Select, plus rules()), then dehydrateState(). Errors are keyed by the column name, which is what the Vue columns read.
  • Only that one attribute is written. beforeStateUpdated/afterStateUpdated still run.
  • Response shapes are unchanged: back() for Inertia; JSON for non-Inertia AJAX requests.

Compatibility with laravilt/tables 1.0.x

Released tables versions do not have EditableColumn or the new column methods. The controller checks interface_exists(EditableColumn::class). When the interface is missing, only ToggleColumn is editable, and the value must pass required|boolean. All other checks (scoping, authorization, single-attribute write) still apply.

Tests

tests/Feature/ColumnUpdateRouteTest.php registers the real route closures and covers:

  • an authorized toggle, including callbacks and the Inertia redirect;
  • 403 for an unauthorized user;
  • a text column rejected with 403, and an unknown column and a disabled column rejected;
  • a non-boolean toggle value rejected with 422;
  • Select option validation;
  • extra request attributes being ignored;
  • tenant scoping;
  • relation route: arbitrary attributes rejected, 403 for an unauthorized user or a read-only relation manager, and a foreign related record rejected;
  • the legacy fallback, simulated by overriding supportsEditableColumns().

Full suite: 133 passed.

🤖 Generated with Claude Code

The resource and relation manager column update routes loaded records
without tenant scoping, skipped update authorization, and wrote any
requested attribute (the relation route with no check at all), so any
panel user could change arbitrary attributes of records they may not edit.

Both routes now go through ColumnStateController, which only writes an
editable, non-disabled, non-relationship column of the relevant table,
loads records through the resource's tenant-scoped query, enforces
canUpdate()/policies (plus the relation manager's canEdit()), validates
the value with the column's rules and writes only that attribute.
Releases of laravilt/tables without EditableColumn fall back to allowing
only ToggleColumn with a boolean value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: cef81982-102a-469d-8a0b-b4a49f1d19de

📥 Commits

Reviewing files that changed from the base of the PR and between 643f0d5 and 95de0fa.

📒 Files selected for processing (3)
  • src/Http/Controllers/ColumnStateController.php
  • src/PanelServiceProvider.php
  • tests/Feature/ColumnUpdateRouteTest.php

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

CI installs laravilt/tables 1.0.x, where the fallback only allows
ToggleColumn, so select option validation cannot be exercised there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fadymondy
fadymondy merged commit 683cd7f into master Sep 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant