
PEACH
Un cadre d’isolation des locataires
The browser helper frond.js, shipped in all four backends (src/public/js/ in tina4php, tina4_python/public/js/, lib/tina4/public/js/, packages/core/public/js/; built from tina4-css src/js/frond.ts), sends the signed-in user's token to other origins and accepts a replacement token from them:
request() (frond.ts:143-146) adds Authorization: Bearer <frond.token> to every URL. Every helper uses it: frond.load, frond.post, frond.form.submit, frond.graphql, and {% live %} poll blocks, whose URL comes from the HTML data-src attribute.request() (frond.ts:172-176) takes a FreshToken response header from any response as the new token, so another origin can replace the user's token for the app's own later requests.form.collect (frond.ts:389-391) writes frond.token into any formToken field. form.submit then posts it to any URL as a multipart body, which needs no CORS preflight, so it reaches any origin, cooperative or not.
On v3 the token frond holds is an identity JWT (FreshToken is issued for identity tokens only), so whoever receives it can act as the user until it expires.The page sets frond.token = "SECRET". The other origin allows Authorization and exposes FreshToken.
frond.load(otherOrigin + "/api", ...): the other origin receives Authorization: Bearer SECRET; the app's next request carries Bearer ATTACKER (its FreshToken).{% live %} block with data-src on the other origin: receives Bearer SECRET.frond.form.submit("f", otherOrigin + "/form") on a form with a rendered form token: body formToken=SECRET.app@other userinfo, tab in the scheme, protocol-relative, leading spaces, upper-case scheme, localhost vs 127.0.0.1) all reach the other origin with the bearer.
Also a functional bug: after one frond.form.submit the form's field holds the identity token, and a later native submit of that form gets 403 from the CSRF middleware (identity token in the formToken slot). Reproduced end to end on a real tina4-python 3.13.157 server with TINA4_CSRF=true: AJAX submit 200, then native submit 403.src/js/frond.ts; then rebuild frond.js and frond.min.js and refresh the vendored copies)responseURL) is on window.location.origin. An explicit Authorization in options.headers still goes wherever the caller sends it.form.collect no longer writes the bearer into formToken; the field goes as the server rendered it. This is safe because all four ports skip CSRF for a valid identity bearer in the header and accept only type: form in the formToken slot (php CsrfMiddleware, python/ruby/node middleware). On the real python server, header identity plus the rendered form token gives 200, and the native submit afterwards gives 200 too.
Checked after the fix, for both frond.js and frond.min.js:src/public/bundled-component-provenance.json needs the two new frond hashes (the release-package check verifies them).
Behaviour change: an app whose API is on another origin (including a subdomain) must pass the Authorization header itself. docs/general/frond.md lines 52-53, 105 and 393 say every request gets the token. They need the same-origin wording, and so does the skills reference templates-and-frontend.md.<details><summary>frond.ts diff</summary>
diff --git a/src/js/frond.ts b/src/js/frond.ts
index a8b8448..242d1e0 100644
--- a/src/js/frond.ts
+++ b/src/js/frond.ts
@@ -110,6 +110,19 @@ interface ManagedStream {
/** Bearer token managed across requests via the FreshToken header. */
let _token: string | null = null;
+/**
+ * True when `url` resolves to the page's own origin. The managed bearer is
+ * sent only there, and a FreshToken is taken only from there, so a call to
+ * another site can neither read the token nor replace it.
+ */
+function _sameOrigin(url: string): boolean {
+ try {
+ return new URL(url, window.location.href).origin === window.location.origin;
+ } catch {
+ return false;
+ }
+}
+
// =====================================================================
// frond.request() — Core HTTP
// =====================================================================
@@ -118,7 +131,8 @@ let _token: string | null = null;
* Perform an XHR request.
*
* Automatically attaches the current Bearer token (if any) and refreshes
- * it from the `FreshToken` response header.
+ * it from the `FreshToken` response header, for same-origin URLs only. To
+ * authenticate to another origin, pass `Authorization` in `options.headers`.
*
* @param url - Request URL.
* @param options - RequestOptions object, or a simple callback function.
@@ -140,8 +154,9 @@ function request(
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
- // Bearer token
- if (_token !== null) {
+ // Bearer token: same origin only, never handed to another site
+ const sameOrigin = _sameOrigin(url);
+ if (_token !== null && sameOrigin) {
xhr.setRequestHeader("Authorization", "Bearer " + _token);
}
@@ -169,9 +184,10 @@ function request(
}
xhr.onload = function () {
- // Rotate token
+ // Rotate token, only from our own origin (also after a redirect)
const freshToken = xhr.getResponseHeader("FreshToken");
- if (freshToken && freshToken !== "") {
+ const answeredBySameOrigin = sameOrigin && (!xhr.responseURL || _sameOrigin(xhr.responseURL));
+ if (freshToken && freshToken !== "" && answeredBySameOrigin) {
_token = freshToken;
}
@@ -370,8 +386,12 @@ const form = {
* Collect all form field values into a FormData object.
*
* Handles inputs, selects, textareas, file uploads (including
- * multi-file), checkboxes, and radio buttons. Updates formToken
- * hidden fields automatically.
+ * multi-file), checkboxes, and radio buttons.
+ *
+ * A `formToken` field is sent as the server rendered it. frond never
+ * writes the bearer into it: the bearer travels in the Authorization
+ * header (own origin only), and the server accepts only a form token in
+ * the formToken slot.
*
* @param formId - DOM id of the form (without '#').
* @returns Populated FormData instance.
@@ -385,11 +405,6 @@ const form = {
for (let i = 0; i < elements.length; i++) {
const el = elements[i] as HTMLInputElement;
- // Inject current token into hidden formToken field
- if (el.name === "formToken" && _token !== null) {
- el.value = _token;
- }
-
if (!el.name) continue;
if (el.type === "file") {</details>
Source: NVD
Évaluation gratuite des vulnérabilités
Évaluez vos pratiques de sécurité cloud dans 9 domaines de sécurité afin d’évaluer votre niveau de risque et d’identifier les lacunes dans vos défenses.
Obtenez une démo personnalisée
"La meilleure expérience utilisateur que j’ai jamais vue, offre une visibilité totale sur les workloads cloud."
"Wiz fournit une interface unique pour voir ce qui se passe dans nos environnements cloud."
"Nous savons que si Wiz identifie quelque chose comme critique, c’est qu’il l’est réellement."