Skip to content

AI blog · DotApp PHP Framework 2.0

How DotApp PHP Framework protects the browser-to-PHP channel

Shop pages that save a contact form, toggle a row, or call a named PHP function do not send a naked POST. The browser talks to PHP on an encrypted, session-bound channel. The working key is not one secret: a per-session key, the application encryption key, extra keys you pass, then a key-update step that compounds them before data is sealed. The framework runs that stack for you. You load the generated script, bind the form, and check integrity in PHP. Copy-paste forms live in How to create secure forms in DotApp PHP Framework. Choosing fo-rm versus load() versus Bridge is Secure backend-frontend communication in DotApp PHP Framework.

Common mistakes

Wrong Right
Treat /assets/dotapp/dotapp.js as a file you copy into the Shop assets folder. Load the framework URL on every page that posts a form, calls load(), or uses Bridge. The route generates the script per client.
Ship a page with no Referer and wonder why the script URL 404s. The generated script route requires a usable Referer. That is an operational fact of the channel, not a feature you turn off.
Put a lone CSRF hidden field on a plain HTML form and call the Shop contact “secure”. Use fo-rm, {{ formName(saveContact) }}, the generated script, crcCheck(), then the matching form() handler.
Reuse one encrypted identifier as both a user id and a product id. Give every identifier field its own extra key (Shop.user.id versus Shop.product.id). Still call Auth::can().
Skip crcCheck() because the browser already “looks” encrypted. PHP is the authority. No integrity check, no trusted body.
Set app.c_enc_key and skip generated dotapp.js / formName. The app key is one layer. The working key also needs the per-session piece, extra keys, and key update. Load the generated script.

What the channel is

The browser-to-PHP channel is the path every Shop UI mutation should take: a contact save, a stock toggle, a Bridge ping. Forms and $dotapp().load() (and Bridge) travel on the same encrypted, session-bound transport. A fo-rm does not make a click “more secure” than load(). Both ride the channel. Both require crcCheck() in PHP. How to pick the surface: Secure backend-frontend communication. Client boot and selectors: How to use $dotapp() JavaScript.

The key stack — high-grade encryption, already wired

This is the part people mean when they ask how hard the channel is to break. DotApp does not seal communication with a single static secret. It builds a working key from several independent pieces, then runs a key-update step that compounds them before any payload is encrypted. That derivation is intentionally demanding. You do not implement it. You load the generated script, use formName / enc / load(), and call crcCheck() in PHP.

  1. A per-session key is generated for this browser session and injected into generated /assets/dotapp/dotapp.js. A new session means a new key. Ciphertext from another session does not open this one.
  2. The application also has a fixed encryption key: app.c_enc_key in app/config.php. Replace the shipped placeholder. It is the long-lived secret of this install. How to set it: How app/config.php works in DotApp PHP Framework.
  3. You add extra keys on identifiers and bindings — Shop.user.id versus Shop.product.id. The same digits under two extra keys are two different ciphertexts. That stops cross-field swap.
  4. Those materials are compounded through key update into one working key. Only then are formName fields, channel payloads, and encrypted ids sealed.
The framework does this for you

This is a high degree of encryption on the browser-to-PHP channel, and Shop code does not assemble the working key by hand. A lone CSRF hidden field is not in the same class: it proves a cookie secret was readable. It does not build this stack. Encryption still is not permission. After decrypt, PHP runs Auth::can() and ownership.

Do not store channel ciphertext in the database expecting another session to decrypt it — the per-session piece is part of the working key. Do not document or reimplement the derivation in a Shop module. Set c_enc_key, unique extra keys, and stay on the public API.

The client script is generated, not static

/assets/dotapp/dotapp.js is not a static file you vendor into app/modules/Shop/assets/. The framework generates it per client and injects per-session random key material. That piece is stacked with app.c_enc_key and your extra keys, then compounded by key update — see the key stack. Without that script, secure posts fail: fo-rm is never converted, CRC and CSRF fields are missing or wrong, and the PHP endpoint rejects the body.

The script route requires a usable Referer. If Referer is missing or too short, the route answers 404. Treat that as an operational fact when you test from unusual clients or strip headers. Do not load a raw copy from app/parts/js/ on production Shop pages. Always include the generated URL before Shop module scripts.


<script src="/assets/dotapp/dotapp.js"></script>
<script src="/assets/modules/Shop/js/contact.js"></script>
    

Pipeline from browser to PHP

One request, several layers. Nothing here is optional polish if you claim the Shop page is on the channel.

  1. The page loads /assets/dotapp/dotapp.js (generated; Referer required or the route 404s).
  2. The script holds the per-session piece. The working key also includes app.c_enc_key, extra keys, and key update.
  3. The renderer has already emitted encrypted hidden fields for each fo-rm. At runtime the script converts fo-rm to a real form and hijacks submit.
  4. On post, the client adds CRC and transport CSRF and sends the body as { data, crc }.
  5. The request carries the header dotapp: load.
  6. PHP runs $request->crcCheck(). Failure means you do not read fields.
  7. On a form, $request->form(['POST'], 'saveContact', ok, err) runs only if the bound handler, URL, and method match.
  8. You answer with ajaxReply. The client decodes it with parseReply.

formName binds handler, URL, and method

{{ formName(saveContact) }} must sit between <fo-rm> and </fo-rm>. The renderer emits encrypted hidden fields that bind three things under a per-form key: the PHP handler name, the action URL, and the HTTP method. Forging or swapping handlers from the HTML is not practical. A field that looks like “saveContact” in the markup is not a string you can edit into “deleteShop”.

If you omit the directive, or place it before <fo-rm> or after </fo-rm>, the renderer leaves the tag unchanged — a silent failure. The handler string in the template must equal the string you pass to form() in PHP. Tiny sketch (Shop contact). Full files: secure forms.


<fo-rm method="POST" id="contactForm" action="{{ var: $postAction }}">
  <input type="text" name="email" />
  {{ formName(saveContact) }}
  <button type="submit" id="contactBtn">Send</button>
</fo-rm>
    

Integrity: crcCheck() on the server

The client posts a payload plus a CRC. PHP crcCheck() is the public gate. If it fails, do not call form(), do not decrypt identifiers, do not write to shop_* tables. After a pass, persist with QueryBuilder or named RAW (WHERE id = :iduser plus ['iduser' => $id]) — still bound. Raw fetch to the same URL will not satisfy the check. That is expected. load() adds CRC, transport CSRF, and the dotapp: load header for you — including when a hijacked fo-rm submits.


$dotapp().load("/shop/items/toggle", "POST", { id: $dotapp(el).attr("data-item") },
  function (raw) { var reply = $dotapp().parseReply(raw); },
  function (code) { /* 400 CRC, 403 CSRF, 404, 429 */ }
);
    

Encrypted identifiers are not authorization

Never put a raw primary key in the browser (value="7", data-id="7"). Encrypt with {{ enc(Shop.user.id): $id }} or Crypto::encrypt((string)$id, 'Shop.user.id'). Decrypt with the same extra key. A wrong key or a damaged token returns false — reject it.

The extra key ($key2) must be unique per identifier field. Ciphertext produced as Shop.user.id cannot be used as Shop.product.id. That extra key is one piece of the working key together with the per-session key, app.c_enc_key, and key update. That stops cross-field mix-ups. It does not prove the visitor may edit that row. If one select lists many users, all under Shop.user.id, swapping one user token for another in that same field still decrypts. PHP still runs Auth::can() and an ownership query. Encryption is not authorization. Treat tokens as session-bound: do not store them in the database expecting another session to decrypt them.

Public call Role
{{ enc(Shop.user.id): $u.id }} Template: ciphertext for this field only
Crypto::encrypt($plain, 'Shop.product.id') PHP: same idea before you put an id in JSON
Crypto::decrypt($cipher, 'Shop.product.id') PHP: false means bad token — stop
Auth::can('Shop.users.edit') Rights. Always, even after a clean decrypt

A lone CSRF field is a narrow guarantee

CSRF as a web term still matters: a cross-site post should not run as the signed-in Shop operator. A lone CSRF hidden field only proves “this session could read a cookie secret”. It does not bind handler, URL, method, or fields. Next to formName plus Bridge it is a narrow guarantee — useful on the transport, not a substitute for the channel. It does not participate in the key stack (per-session key, application key, extra keys, key update).

Mechanism Lone CSRF hidden field formName + channel
Proves the session could read a cookie secret Yes (weakly) Yes, plus transport CSRF
Binds a specific PHP handler name No Yes (encrypted)
Binds action URL and HTTP method No Yes
Per-form key No Yes
Payload integrity No Yes — crcCheck()
Session-bound client keys via generated dotapp.js No Yes

Named PHP functions from JS use the same channel. Binding, rate limits, and template on(click): How to call PHP from JavaScript with DotBridge.

Layers in words

Stack them. Do not pick one and skip the rest.

Layer What it protects What it does not
Generated dotapp.js Per-session key material for this browser session Who may edit a Shop row
app.c_enc_key + extra keys + key update The working key that seals payloads and identifiers Authorization — still Auth::can()
fo-rm / load() / Bridge + crcCheck() Transport: tamper, wrong handler, missing integrity The meaning of id=7
Unique $key2 on every identifier Cross-field mix-up (user ciphertext ≠ product ciphertext) Ownership — still Auth::can()
Transport CSRF A narrow “this session could submit this post” Handler, URL, method, or field binding
PHP Auth::can() / ownership Authorization The wire format — you still crcCheck() first
PHP is the authority

Overlays, disabled buttons, and confirm dialogs are UX. The handler that persists the change still crcCheck() once, then decrypt, rights, and validation. Do not call crcCheck() in a global before-hook and again in that handler — the first success burns the one-time token (Request lifecycle). A visitor who posts without the overlay must still be refused, and the previous Shop row must stay unchanged.

FAQ

Can I copy dotapp.js into the Shop module?

No. The production URL is generated per client and injects session key material. A copied file has no keys for this session. Secure posts fail. Load /assets/dotapp/dotapp.js.

Why does the script URL 404 in my test client?

The route requires a usable Referer. If the header is missing or shorter than the framework expects, you get 404. Fix the client so the header is present. Do not treat the 404 as a missing static asset.

Is a CSRF token enough for the Shop contact form?

No. A lone CSRF field only proves this session could read a cookie secret. It does not bind saveContact, the action URL, the method, or the fields. Use formName on the channel. It also does not build the working key. That is the key stack.

How strong is the encryption?

The working key is not one secret. A per-session key, the fixed application key, extra keys you pass, and a key-update step that compounds them come first. That derivation is intentionally demanding. The framework runs it when you stay on the public API. Do not reimplement it in Shop. Do not treat a copied dotapp.js as the same protection.

Does fo-rm protect more than load()?

No. Both use CRC, transport CSRF, and the dotapp: load header. Use fo-rm when the user fills several fields and submits. Use load() for a click, toggle, delete, or pager. Details: backend-frontend communication.

If ids are encrypted, can I skip Auth::can()?

No. Unique extra keys stop a product token from decrypting as a user id. They do not decide whether this operator may change that user. Encrypt, then authorize.

Should browser UI return JSON with Response::json?

Channel replies use ajaxReply (the client calls parseReply). Ordinary JSON HTTP endpoints that are not this channel use Router plus Response::json. Do not mix the two on the same Shop action.

Is Bridge a different pipe?

Bridge is discrete named PHP functions from JS or template on(click). It still rides the encrypted session-bound channel. Walkthrough: DotBridge.

What happens if I forget the script tag?

fo-rm stays as an unknown tag, no CRC/CSRF is added, and PHP crcCheck() fails. Do not claim that page is on the DotApp channel.

See also