27d1165962
gates / gates (push) Successful in 17s
Site one. app_info.html rendered {{.InitialCreds.Password}} into a hidden span —
a REAL per-install credential, read live out of the running container, in the
response body of every render. The page now carries the non-secret half plus a
boolean; the value comes from POST /apps/<slug>/initial-credentials/reveal, which
RE-READS the container rather than serving a cached copy (caching it in the
handler would put it back in the body one layer in). no-store, CSRF-covered,
logged as an act. Both buttons go through it. A reveal that cannot read the value
SAYS SO rather than returning an empty string that renders as a blank password.
Site two, established before changing. The hidden input is NOT the defect and was
left alone: it fires only pre-deploy, and README §318 documents why the value must
round-trip — the customer notes the generated secrets down and submitting them
back is what makes the saved value the same one they saw. The defect was the
neighbouring READONLY input, which on an ALREADY-DEPLOYED app rendered the secret
into a page with nothing to submit. Fixed by POST /stacks/<name>/auto-field/reveal,
authorised by requiring a type:secret auto-field of that stack. Both directions
pinned.
The premise that this contradicted a repo rule does not hold: the rule is
CONTEXT.md:2070 'Password fields require explicit input — prevents accidental
empty-password deployments', about EMPTINESS. No line in the repo says 'no silent
auto-fill'.
The gate. scripts/secret_in_markup_gate.py, registered in controller_gates.py,
convicts any template expression that names a secret unless allowlisted with a
reason. Its limits are MEASURED and in its docstring: it catches a launder through
a local variable (the assignment names the secret) but is blind to a secret
arriving under a neutral page-data key — verified both ways. That is the shape of
site two, which this gate would NOT have caught. The runtime body assertion covers
all shapes but only 4 of 27 page templates; the other 23 are R-255, filed rather
than glossed. Two nets, different holes, both named.
Correction to v0.207.0's report: HTML comments do NOT ship in the response body
here — html/template strips them, text/template does not. Measured. A red-proof
planting a secret in a comment therefore correctly does not fail.
87 lines
4.1 KiB
Go
87 lines
4.1 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// R-254 site two — WHAT §7.2 ESTABLISHED, PINNED SO IT CANNOT DRIFT BACK.
|
|
//
|
|
// The deploy page has TWO places a generated secret can appear, and they are NOT the same question:
|
|
//
|
|
// - the PRE-DEPLOY hidden input — a form must carry what it submits. README §318 documents why:
|
|
// the customer is shown the generated secrets so they can note them down, and submitting them
|
|
// back is what makes the saved value the SAME one they saw ("no silent re-generation on submit").
|
|
// This is NOT the defect and is deliberately left alone.
|
|
// - the READONLY display input on an ALREADY-DEPLOYED app — nothing is being submitted there (the
|
|
// hidden input is correctly omitted), yet the value was rendered into the body of a page the
|
|
// customer merely opens. That IS R-249's shape, and it is what v0.208.0 fixes.
|
|
//
|
|
// Both directions are asserted, because "fixed" here means one branch changed and the other did not.
|
|
|
|
const testDeploySecret = "TESTONLY-generated-db-pw-Xy91"
|
|
|
|
func renderDeployPage(t *testing.T, alreadyDeployed bool) string {
|
|
t.Helper()
|
|
s := securityHarness(t)
|
|
s.loadTemplates()
|
|
data := map[string]interface{}{
|
|
"Page": "stacks", "Title": "Telepítés", "Domain": "example.hu",
|
|
"Stack": stacks.Stack{Name: "vaultwarden", Deployed: alreadyDeployed},
|
|
"Meta": stacks.Metadata{DisplayName: "Vaultwarden", Slug: "vaultwarden"},
|
|
"AlreadyDeployed": alreadyDeployed,
|
|
"AutoFields": []stacks.DeployField{
|
|
{EnvVar: "DB_PASSWORD", Label: "Adatbázis jelszó", Type: "secret"},
|
|
},
|
|
"AutoFieldValues": map[string]string{"DB_PASSWORD": testDeploySecret},
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := s.tmpl.ExecuteTemplate(&buf, "deploy", data); err != nil {
|
|
t.Fatalf("render deploy: %v", err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// RED-PROOF: drop the `{{if $isDeployed}}` branch so the deployed page renders `value="{{$val}}"`
|
|
// again — this fails, showing the secret returning to the body of a page with nothing to submit.
|
|
func TestDeployPage_DeployedApp_DoesNotCarryTheSecret(t *testing.T) {
|
|
html := renderDeployPage(t, true)
|
|
|
|
if strings.Contains(html, testDeploySecret) {
|
|
t.Error("R-254 site two: an already-deployed app's generated secret is in the response body " +
|
|
"of its settings page — nothing there submits it, so there is no form reason for it to " +
|
|
"be in the page at all")
|
|
}
|
|
// Assert the CONTROL, not the URL: the revealAutoField() function ships in the page script on
|
|
// both variants, so a substring match on the endpoint path matches the script and would report a
|
|
// control that is not there. (This test caught exactly that on itself.)
|
|
if !strings.Contains(html, `onclick="revealAutoField('vaultwarden','DB_PASSWORD'`) {
|
|
t.Error("no reveal control rendered, so the customer cannot see their own generated secret")
|
|
}
|
|
// The hidden input must NOT appear on a deployed app — it never did, and that is the asymmetry
|
|
// that makes the readonly input indefensible there.
|
|
if strings.Contains(html, `<input type="hidden" name="DB_PASSWORD"`) {
|
|
t.Error("a submit-carrying hidden input rendered on an already-deployed app")
|
|
}
|
|
}
|
|
|
|
// The OTHER half of §7.2's answer: the pre-deploy form still carries the value, deliberately. If this
|
|
// ever starts failing, someone has "fixed" a form by stopping it submitting what it must submit —
|
|
// which would silently re-generate the secret on save and hand the customer a password that is not
|
|
// the one they wrote down.
|
|
func TestDeployPage_PreDeployForm_StillCarriesTheValue_Deliberately(t *testing.T) {
|
|
html := renderDeployPage(t, false)
|
|
|
|
if !strings.Contains(html, `<input type="hidden" name="DB_PASSWORD" value="`+testDeploySecret+`">`) {
|
|
t.Error("the pre-deploy form no longer submits the generated secret — the saved value would " +
|
|
"then not be the one the customer was shown (README §318, 'no silent re-generation on submit')")
|
|
}
|
|
if strings.Contains(html, `onclick="revealAutoField(`) {
|
|
t.Error("the deployed-app reveal control leaked onto the pre-deploy form, where the value is " +
|
|
"already legitimately present")
|
|
}
|
|
}
|