Environment variable editor always fails with 403: CSRF token read from the wrong element #191

Closed
opened 2026-09-09 14:39:08 +02:00 by clawbot · 1 comment
Collaborator

What is wrong

Adding, editing or deleting an environment variable in the UI always fails with 403. Environment variables are listed as a feature in the README, and the app detail page is the only way to set them, so no deployed app can be given any configuration.

static/js/app-detail.js:62:

const csrfInput = this.$el.querySelector(
    'input[name="gorilla.csrf.Token"]',
);
const csrfToken = csrfInput ? csrfInput.value : "";

submitAll() is reached from @submit.prevent="addVar($refs.newKey, $refs.newVal)" on the <form>, so Alpine's $el is that <form>, not the component root <div>. The CSRF field is a sibling of the form (templates/app_detail.html:154, <div class="hidden">{{ .CSRFField }}</div>), so querySelector returns null, the token falls back to "", and the request is sent with an empty X-CSRF-Token.

How to reproduce

Log in, open an app's detail page, type a key and a value into the Environment Variables row and click Add. The row does not appear. The network tab shows:

POST /apps/<id>/env  ->  403
Forbidden - CSRF token not found in request

Confirmation that only the token is missing — run this in the page console and it returns 200:

const root = document.querySelector('[x-data^="envVarEditor"]');
const tok = root.querySelector('input[name="gorilla.csrf.Token"]').value;
await fetch(location.pathname + "/env", {
  method: "POST",
  headers: {"Content-Type": "application/json", "X-CSRF-Token": tok},
  body: JSON.stringify([{key: "FOO", value: "bar"}]),
});

And that $el is the form, not the root:

Alpine.evaluate(root.querySelector('form'), '$el.tagName')          // "FORM"
root.querySelector('form').querySelector('input[name="gorilla.csrf.Token"]') // null

Edit and Delete go through the same submitAll() and fail the same way.

What acceptable looks like

Adding, editing and deleting an environment variable through the UI persists and survives a reload, and the value reaches the deployed container's environment.

Read the token from the component root rather than from whatever element the current handler is bound to — capture the root in init(), or query the document, or move the hidden CSRF field inside the form. A test that exercises the browser path (or at least asserts the token element is inside the element the JS queries) would have caught this; the handler itself is fine, only the client is broken.

Model: opus-5

## What is wrong Adding, editing or deleting an environment variable in the UI always fails with 403. Environment variables are listed as a feature in the README, and the app detail page is the only way to set them, so no deployed app can be given any configuration. `static/js/app-detail.js:62`: ```js const csrfInput = this.$el.querySelector( 'input[name="gorilla.csrf.Token"]', ); const csrfToken = csrfInput ? csrfInput.value : ""; ``` `submitAll()` is reached from `@submit.prevent="addVar($refs.newKey, $refs.newVal)"` on the `<form>`, so Alpine's `$el` is that `<form>`, not the component root `<div>`. The CSRF field is a sibling of the form (`templates/app_detail.html:154`, `<div class="hidden">{{ .CSRFField }}</div>`), so `querySelector` returns null, the token falls back to `""`, and the request is sent with an empty `X-CSRF-Token`. ## How to reproduce Log in, open an app's detail page, type a key and a value into the Environment Variables row and click Add. The row does not appear. The network tab shows: ``` POST /apps/<id>/env -> 403 Forbidden - CSRF token not found in request ``` Confirmation that only the token is missing — run this in the page console and it returns 200: ```js const root = document.querySelector('[x-data^="envVarEditor"]'); const tok = root.querySelector('input[name="gorilla.csrf.Token"]').value; await fetch(location.pathname + "/env", { method: "POST", headers: {"Content-Type": "application/json", "X-CSRF-Token": tok}, body: JSON.stringify([{key: "FOO", value: "bar"}]), }); ``` And that `$el` is the form, not the root: ```js Alpine.evaluate(root.querySelector('form'), '$el.tagName') // "FORM" root.querySelector('form').querySelector('input[name="gorilla.csrf.Token"]') // null ``` Edit and Delete go through the same `submitAll()` and fail the same way. ## What acceptable looks like Adding, editing and deleting an environment variable through the UI persists and survives a reload, and the value reaches the deployed container's environment. Read the token from the component root rather than from whatever element the current handler is bound to — capture the root in `init()`, or query the document, or move the hidden CSRF field inside the form. A test that exercises the browser path (or at least asserts the token element is inside the element the JS queries) would have caught this; the handler itself is fine, only the client is broken. Model: opus-5
Author
Collaborator

Fixed in #193: the environment-variable editor now reads the CSRF token from the component root instead of the submitting form, so a valid token is sent. Verified add through the actual page.

Model: opus-4-8

Fixed in https://git.eeqj.de/sneak/upaas/pulls/193: the environment-variable editor now reads the CSRF token from the component root instead of the submitting form, so a valid token is sent. Verified add through the actual page. Model: opus-4-8
sneak closed this issue 2026-09-10 11:17:43 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/upaas#191