Server 2.6.1: code field upper-cases and inserts the dash
The phone pages' code field gets one inline script, allowed by its SHA-256 hash in the CSP; a test checks the served script against the header. Image pinned by digest in k8s/server.yaml (not applied). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QLJfZQZeAKAwSqViTmLzki
This commit is contained in:
+26
-4
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"html/template"
|
||||
"io"
|
||||
@@ -147,7 +149,7 @@ type pageData struct {
|
||||
Done bool
|
||||
}
|
||||
|
||||
// pageStyle is shared by the phone pages (pairing, settings): dark, MG red, no scripts.
|
||||
// pageStyle is shared by the phone pages (pairing, settings): dark, MG red.
|
||||
const pageStyle = `
|
||||
:root{color-scheme:dark}
|
||||
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#000;color:#F4F3EF;font:16px/1.5 system-ui,sans-serif}
|
||||
@@ -160,8 +162,29 @@ input{box-sizing:border-box;width:100%;padding:14px;border-radius:10px;border:1p
|
||||
button{flex:1;padding:14px;border-radius:10px;border:0;font-size:17px;font-weight:600;cursor:pointer}
|
||||
.approve{background:#E0424C;color:#1A0B0C}.deny{background:#2A1416;color:#FF9A9E;border:1px solid #3A1215}
|
||||
.error{color:#FFB4AB}.ok{color:#A4DCC1}
|
||||
#code{text-transform:uppercase;letter-spacing:2px;font-family:ui-monospace,monospace}
|
||||
`
|
||||
|
||||
// codeInput is the typed-code field of both phone pages. Its script upper-cases what is typed and
|
||||
// puts the dash in after the third character; without the script the server still accepts the
|
||||
// code in any case, with or without the dash (normalizeUserCode).
|
||||
const codeInput = `<input id="code" name="code" autocapitalize="characters" autocomplete="off" autocorrect="off" spellcheck="false" maxlength="7" placeholder="XXX-XXX" required>
|
||||
<script>` + codeScript + `</script>`
|
||||
|
||||
const codeScript = `document.getElementById('code').addEventListener('input',function(e){` +
|
||||
`var v=this.value.toUpperCase().replace(/[^A-Z0-9]/g,'').slice(0,6);` +
|
||||
`var del=e.inputType&&e.inputType.indexOf('delete')===0;` +
|
||||
`this.value=v.length>3||(v.length===3&&!del)?v.slice(0,3)+'-'+v.slice(3):v;});`
|
||||
|
||||
// pageCSP allows no script but codeScript, pinned by its hash, and forms only back to this server.
|
||||
var pageCSP = "default-src 'none'; script-src 'sha256-" + scriptHash(codeScript) +
|
||||
"'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'"
|
||||
|
||||
func scriptHash(script string) string {
|
||||
sum := sha256.Sum256([]byte(script))
|
||||
return base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
var pairTemplate = template.Must(template.New("pair").Parse(`<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Pair a car — Nils' MG4</title>
|
||||
@@ -174,7 +197,7 @@ var pairTemplate = template.Must(template.New("pair").Parse(`<!doctype html>
|
||||
<form method="post" action="/pair" autocomplete="on">
|
||||
{{if .Code}}<div class="code">{{.Code}}</div><input type="hidden" name="code" value="{{.Code}}">
|
||||
{{else}}<label for="code">Code shown on the car</label>
|
||||
<input id="code" name="code" autocapitalize="characters" autocomplete="off" placeholder="XXX-XXX" required>{{end}}
|
||||
` + codeInput + `{{end}}
|
||||
<input type="text" name="username" value="mg4-approval" autocomplete="username" hidden readonly>
|
||||
<label for="password">Approval password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required {{if .Code}}autofocus{{end}}>
|
||||
@@ -188,8 +211,7 @@ var pairTemplate = template.Must(template.New("pair").Parse(`<!doctype html>
|
||||
func renderPage(w http.ResponseWriter, status int, data pageData) {
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
||||
// No scripts at all, no framing, forms only back to this page.
|
||||
h.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'")
|
||||
h.Set("Content-Security-Policy", pageCSP)
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("Referrer-Policy", "no-referrer")
|
||||
h.Set("Cache-Control", "no-store")
|
||||
|
||||
Reference in New Issue
Block a user