Security: Blacklisting Tables & Columns
The real security boundary lives in src/Config.php, not in any individual page's ->columns() call.
->columns() is not a security boundary
Calling ->columns('name', 'email') on a page only changes what that page's own grid displays. It does nothing to core/ajax_crud.php, the REST endpoint every widget's JavaScript actually talks to - the API resolves a page's configuration by re-running its route (xcrud_load_hooks_from_page()), but the columns a real database table exposes are governed by the table's own schema, not by what one page chose to show. A visitor who calls the API directly (open DevTools, replay the request with a different route/query) is not restricted by any page's own ->columns() narrowing.
In other words: ->columns() is a display convenience. It is never the thing standing between a sensitive column and the network.
$blacklistedTables - the real table boundary
Every real table in the connected database is reachable through Xcrud by default (live-introspected, nothing to register first) - $blacklistedTables in src/Config.php is what removes a table from reach entirely, no matter what any page's own PHP tries:
public static array $blacklistedTables = [
'sys_settings', 'sys_settings_other', 'sys_notifications', 'logs',
];
A blacklisted table can never be reached through ->table(), as a ->relation() target, or as a ->gallery() child table - calling ->table('logs') throws immediately rather than silently rendering an empty grid.
$blacklistedColumns - column-level blocking
Some tables are fine to expose generally but have specific columns that should never round-trip through the API - a password hash, an internal token, a legacy blob column. $blacklistedColumns blocks those columns within an otherwise-reachable table, keyed by table name:
public static array $blacklistedColumns = [
'user' => [
'password', 'confirm_password', 'session',
'remember_token', 'remember_token_expires',
'confirmation_code', 'app_key',
],
'customers' => ['avatar'],
'productlines' => ['image'],
];
A column listed here is hidden and blocked from writes - it is never returned in a list/form JSON response and never accepted on insert/update, regardless of what any page's own ->columns()/->fields() call asks for. This is what actually keeps a table like user safe to expose at all: the table itself doesn't need blacklisting just because one sensitive column on it does.
$appSecret - signing, not blacklisting
A related but separate concern: $appSecret HMAC-signs ->where()/->subselect()/->pass_var() conditions and logic-condition payloads before they travel through the browser, so ajax_crud.php can refuse to apply one whose signature doesn't match. It protects a page's own intentional scoping (e.g. "only show this customer's own orders") from being tampered with client-side - it does not replace the table/column blacklist above. See Deploying to Production for generating a strong value before launch.