GHSA-GJ2H-2FPW-FHV9

Vulnerability from github – Published: 2026-07-02 20:16 – Updated: 2026-08-04 21:55
VLAI
Summary
@nuxt/ui: UAuthForm / UForm SSR markup omits `method`, leaking credentials via GET if submitted before hydration
Details

Summary

UForm and UAuthForm render a server-side <form> element with no method and no action attribute, relying on a hydrated @submit.prevent handler to intercept submission. If a user submits the form before Vue hydration has attached the handler (autofill plus Enter on a slow network, JS bundle blocked by CSP or CDN failure, etc.), the browser performs the native default: a GET to the current URL with every named field, including <input type="password">, serialised into the query string.

Details

src/runtime/components/Form.vue (around the template's <form> element) emits:

<component
  :is="parentBus ? 'div' : 'form'"
  :id="formId"
  ref="formRef"
  :class="ui({ class: [uiProp?.base, props.class] })"
  @submit.prevent="onSubmitWrapper"
>

No method, no action. @submit.prevent is the only thing stopping native submission, and it only exists after hydration. UAuthForm composes UForm and inherits the same shape.

The SSR snapshot of UAuthForm (test/components/__snapshots__/AuthForm.spec.ts.snap) shows the rendered markup, with <input type="password" name="password"> inside a <form> that has no method.

Proof of concept

Reported by @nimonian:

  1. Create a minimal Nuxt app with a UAuthForm.
  2. Build for production and visit in a browser with network throttling at 4G or slower.
  3. Enter credentials.
  4. Submit (or let autofill + Enter fire before hydration).

The URL becomes /login?email=…&password=…. Reproducible deterministically in Playwright by triggering submit immediately on load.

Impact

Any application using UAuthForm (or UForm with credential-shaped fields) as documented. The cleartext password lands in:

  • the address bar,
  • window.history,
  • the Referer header of every same-origin subresource fetched from the resulting URL,
  • access logs of any reverse proxy, CDN, or WAF that records request URLs.

Patch

Default the rendered <form> to method="post" so the pre-hydration fallback submits as POST rather than GET. Vue's @submit.prevent still intercepts the hydrated case; the attribute only matters in the race window. Applications that explicitly want native GET submission can opt back in by passing method="get".

Credit

Reported by @nimonian. Originally filed as GHSA-92g7-2fpq-hmq8 against nuxt/nuxt; moved here because the affected code lives in @nuxt/ui.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "@nuxt/ui"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.8.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-598"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-02T20:16:12Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\n`UForm` and `UAuthForm` render a server-side `\u003cform\u003e` element with no `method` and no `action` attribute, relying on a hydrated `@submit.prevent` handler to intercept submission. If a user submits the form before Vue hydration has attached the handler (autofill plus Enter on a slow network, JS bundle blocked by CSP or CDN failure, etc.), the browser performs the native default: a `GET` to the current URL with every named field, including `\u003cinput type=\"password\"\u003e`, serialised into the query string.\n\n### Details\n\n`src/runtime/components/Form.vue` (around the template\u0027s `\u003cform\u003e` element) emits:\n\n```vue\n\u003ccomponent\n  :is=\"parentBus ? \u0027div\u0027 : \u0027form\u0027\"\n  :id=\"formId\"\n  ref=\"formRef\"\n  :class=\"ui({ class: [uiProp?.base, props.class] })\"\n  @submit.prevent=\"onSubmitWrapper\"\n\u003e\n```\n\nNo `method`, no `action`. `@submit.prevent` is the only thing stopping native submission, and it only exists after hydration. `UAuthForm` composes `UForm` and inherits the same shape.\n\nThe SSR snapshot of `UAuthForm` (`test/components/__snapshots__/AuthForm.spec.ts.snap`) shows the rendered markup, with `\u003cinput type=\"password\" name=\"password\"\u003e` inside a `\u003cform\u003e` that has no `method`.\n\n### Proof of concept\n\nReported by @nimonian:\n\n1. Create a minimal Nuxt app with a `UAuthForm`.\n2. Build for production and visit in a browser with network throttling at 4G or slower.\n3. Enter credentials.\n4. Submit (or let autofill + Enter fire before hydration).\n\nThe URL becomes `/login?email=\u2026\u0026password=\u2026`. Reproducible deterministically in Playwright by triggering submit immediately on `load`.\n\n### Impact\n\nAny application using `UAuthForm` (or `UForm` with credential-shaped fields) as documented. The cleartext password lands in:\n\n- the address bar,\n- `window.history`,\n- the `Referer` header of every same-origin subresource fetched from the resulting URL,\n- access logs of any reverse proxy, CDN, or WAF that records request URLs.\n\n### Patch\n\nDefault the rendered `\u003cform\u003e` to `method=\"post\"` so the pre-hydration fallback submits as POST rather than GET. Vue\u0027s `@submit.prevent` still intercepts the hydrated case; the attribute only matters in the race window. Applications that explicitly want native GET submission can opt back in by passing `method=\"get\"`.\n\n### Credit\n\nReported by @nimonian. Originally filed as `GHSA-92g7-2fpq-hmq8` against `nuxt/nuxt`; moved here because the affected code lives in `@nuxt/ui`.",
  "id": "GHSA-gj2h-2fpw-fhv9",
  "modified": "2026-08-04T21:55:59Z",
  "published": "2026-07-02T20:16:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/ui/security/advisories/GHSA-gj2h-2fpw-fhv9"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/ui/pull/6512"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nuxt/ui"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/ui/releases/tag/v4.8.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "@nuxt/ui: UAuthForm / UForm SSR markup omits `method`, leaking credentials via GET if submitted before hydration"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.