v0.150.0 — green gate restored + the export link stops leaking the CSRF token
F7/R-53: app_export.html built the app's public URL as '<sub>.{{$.CSRFToken}}',
so the "Megnyitás" link was wrong for every app with a subdomain and a session
CSRF token was written into a URL. Template now uses {{$.Domain}}, and
exportPageHandler supplies the key — it builds its own data map instead of
going through baseData, which is where every other page gets it. The page's
real CSRF path (csrfH() reading the meta tag) is correct and untouched.
The 7 red internal/backup tests are green again, with no behaviour change.
TestTier2V2_* / TestSharesTier2* all failed for one environmental reason:
Tier-2's off-drive guard asks system.SamePhysicalDevice (st_dev equality)
whether a target is really a second disk, and every t.TempDir() here shares one
filesystem — so the guard correctly refused the fixture's "two drives" and the
tests never reached their subject ("nincs másik fizikai meghajtó").
Seam in the package's existing style: a nil-defaulted Manager.samePhysicalDevice
field + sameDevice wrapper, seven call sites routed through it. Nil resolves to
system.SamePhysicalDevice, so production is byte-for-byte unchanged; only the two
fixtures inject a fake modelling one drive per directory subtree. No assertion
weakened, nothing skipped/renamed/deleted; all 7 mutation-proved.
Also: the ssh->pct-exec ASCII-grep and heredoc-credential traps are now in
CLAUDE.md's live-validation section.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// R-53 / F7: app_export.html built the app's public URL as `<subdomain>.{{$.CSRFToken}}` — the
|
||||
// session CSRF token substituted where the customer domain belongs. That is two defects in one
|
||||
// token: the „open in browser" link is wrong for every app that has a subdomain, and a CSRF token
|
||||
// lands in a URL (history, referrers, logs). The correct CSRF usage on this page is the csrfH()
|
||||
// helper reading the meta tag, which is untouched.
|
||||
|
||||
const exportTestToken = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe"
|
||||
|
||||
// exportScriptLine returns the `var domain = …` line, so an assertion cannot accidentally match the
|
||||
// token where it legitimately appears (the meta tag) elsewhere on the page.
|
||||
func exportScriptLine(t *testing.T, html string) string {
|
||||
t.Helper()
|
||||
for _, ln := range strings.Split(html, "\n") {
|
||||
if strings.Contains(ln, "var domain =") {
|
||||
return ln
|
||||
}
|
||||
}
|
||||
t.Fatalf("no `var domain =` line in the rendered export page")
|
||||
return ""
|
||||
}
|
||||
|
||||
func renderExport(t *testing.T, subdomain string) string {
|
||||
t.Helper()
|
||||
return renderBackupPage(t, "app_export", map[string]interface{}{
|
||||
"Stack": stacks.Stack{
|
||||
Name: "immich", Deployed: true,
|
||||
Meta: stacks.Metadata{Slug: "immich", DisplayName: "Immich", Subdomain: subdomain},
|
||||
},
|
||||
"Drives": nil,
|
||||
"Domain": "demo-felhom.eu",
|
||||
"CSRFToken": exportTestToken,
|
||||
"CSRFField": "",
|
||||
"Page": "apps",
|
||||
"Title": "Export",
|
||||
"ExportPage": true,
|
||||
})
|
||||
}
|
||||
|
||||
// Scenario C — the domain is joined from the CUSTOMER DOMAIN, and the CSRF token appears nowhere in
|
||||
// that line. COMPANION red-proof: restore `{{$.CSRFToken}}` in app_export.html's `var domain` line
|
||||
// → both assertions FAIL. Run → fail → revert (recorded in REPORT).
|
||||
func TestAppExportDomainUsesCustomerDomainNotCSRFToken(t *testing.T) {
|
||||
line := exportScriptLine(t, renderExport(t, "photos"))
|
||||
|
||||
if !strings.Contains(line, "'photos.demo-felhom.eu'") {
|
||||
t.Errorf("export link must be built from the customer domain, got: %s", line)
|
||||
}
|
||||
if strings.Contains(line, exportTestToken) {
|
||||
t.Errorf("the CSRF token must NEVER appear in the export URL, got: %s", line)
|
||||
}
|
||||
}
|
||||
|
||||
// The empty-subdomain branch still yields '' — an app without a subdomain must not get a link to
|
||||
// a bare domain (the truthiness guard is what produces that, and the fix must not disturb it).
|
||||
func TestAppExportDomainEmptyWithoutSubdomain(t *testing.T) {
|
||||
line := exportScriptLine(t, renderExport(t, ""))
|
||||
|
||||
if !strings.Contains(line, "''") {
|
||||
t.Errorf("no subdomain must yield an empty domain, got: %s", line)
|
||||
}
|
||||
if strings.Contains(line, "demo-felhom.eu'") && !strings.Contains(line, "'' ? ") {
|
||||
t.Errorf("no subdomain must not produce a bare-domain link, got: %s", line)
|
||||
}
|
||||
if strings.Contains(line, exportTestToken) {
|
||||
t.Errorf("the CSRF token must NEVER appear in the export URL, got: %s", line)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +103,10 @@ func (s *Server) exportPageHandler(w http.ResponseWriter, r *http.Request, name
|
||||
data := map[string]interface{}{
|
||||
"Stack": stack,
|
||||
"Drives": drives,
|
||||
// R-53: the page builds the app's public URL from the customer domain. This handler does not
|
||||
// go through baseData (which is where every other page gets "Domain"), so it must supply the
|
||||
// key itself — the template used to substitute the CSRF token here instead.
|
||||
"Domain": s.cfg.Customer.Domain,
|
||||
}
|
||||
s.executeTemplate(w, r, "app_export", data)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
|
||||
<script>
|
||||
var stackName = '{{.Stack.Name}}';
|
||||
var domain = '{{.Stack.Meta.Subdomain}}' ? '{{.Stack.Meta.Subdomain}}.{{$.CSRFToken}}' : '';
|
||||
var domain = '{{.Stack.Meta.Subdomain}}' ? '{{.Stack.Meta.Subdomain}}.{{$.Domain}}' : '';
|
||||
var pollTimer = null;
|
||||
|
||||
function csrfH() {
|
||||
|
||||
Reference in New Issue
Block a user