Vulnerability DatabaseGHSA-q2gc-fpw5-3v3c

GHSA-q2gc-fpw5-3v3c: 
JavaScript vulnerability analysis and mitigation

Summary

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:

  1. 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.
  2. 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.
  3. 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.

Reproduction (headless Chromium, two loopback origins, 3.13.157 assets)

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.
  • Nine URL shapes (double backslash, 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.

Fix (in tina4-css src/js/frond.ts; then rebuild frond.js and frond.min.js and refresh the vendored copies)

  • Send the managed bearer, and take a FreshToken, only when the URL (and, for FreshToken, the final 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:
  • nothing reaches the other origin on any of the paths above;
  • same-origin bearer and FreshToken rotation still work. The tina4-css jsdom suite gets 9 new tests (one real server per origin). All 14 pass, and the 8 that cover cross-origin and formToken behaviour fail on the current source. In tina4php, 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

Related JavaScript vulnerabilities:

CVE ID

Severity

Score

Technologies

Component name

CISA KEV exploit

Has fix

Published date

CVE-2026-120415MEDIUM5.9
  • JavaScript logoJavaScript
  • undici
NoYesNov 05, 2026
CVE-2026-116630MEDIUM5.9
  • JavaScript logoJavaScript
  • undici
NoYesNov 05, 2026
GHSA-9rmh-cqxg-g7fmLOWN/A
  • JavaScript logoJavaScript
  • tina4-nodejs
NoYesNov 05, 2026
GHSA-685j-v6qp-54w7LOWN/A
  • JavaScript logoJavaScript
  • @tina4/core
NoYesNov 05, 2026
GHSA-q2gc-fpw5-3v3cMEDIUMN/A
  • JavaScript logoJavaScript
  • tina4-python
NoYesNov 05, 2026

Free Vulnerability Assessment

Benchmark your Cloud Security Posture

Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.

Request assessment

Additional Wiz resources

Get a personalized demo

Ready to see Wiz in action?

"Best User Experience I have ever seen, provides full visibility to cloud workloads."
David EstlickCISO
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
Adam FletcherChief Security Officer
"We know that if Wiz identifies something as critical, it actually is."
Greg PoniatowskiHead of Threat and Vulnerability Management