/

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.