adminui: the enrolment link was rendered as a dead anchor
html/template rewrites an href whose scheme it does not recognise to "#ZgotmplZ". echolot:// is not on its list, so "Open in the Echolot app" was not a link at all — tapping it did nothing, and nothing showed it: the markup reads correctly, the app resolves the scheme, and only the sanitised attribute in the served HTML gives it away. Marking the value template.URL opts out of that sanitising, which is only safe because the shape is now checked first. The link arrives in a query parameter, so without the check a crafted /devices?link=javascript:… would put a script URL into the page for an admin to click. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f6e093944c
commit
33799b8135
@@ -5,10 +5,12 @@ package adminui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"echo-lot.app/server/internal/adminauth"
|
||||
@@ -122,9 +124,21 @@ func (s *Server) devices(w http.ResponseWriter, r *http.Request, sess *adminauth
|
||||
}
|
||||
rows = append(rows, row{Device: d, Runs: n})
|
||||
}
|
||||
// html/template rewrites an href whose scheme it does not recognise to "#ZgotmplZ", so the
|
||||
// enrollment link rendered as a dead anchor that did nothing when tapped — silently, since the
|
||||
// markup looks fine and only the sanitised attribute gives it away.
|
||||
//
|
||||
// Marking it template.URL opts out of that sanitising, which is only safe because the shape is
|
||||
// checked first: this value arrives in a query parameter, so without the check a crafted
|
||||
// /devices?link=javascript:… would put a script URL straight into the page.
|
||||
link := r.URL.Query().Get("link")
|
||||
var href template.URL
|
||||
if strings.HasPrefix(link, "echolot://enroll?") {
|
||||
href = template.URL(link)
|
||||
}
|
||||
s.render(w, r, "devices", map[string]any{
|
||||
"Session": sess, "CSRF": s.csrfToken(sess), "Rows": rows,
|
||||
"Link": r.URL.Query().Get("link"), "Admin": sess.Admin,
|
||||
"Link": link, "LinkHref": href, "Admin": sess.Admin,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,7 @@ const baseHTML = `<!doctype html>
|
||||
<!-- On the phone being enrolled this is the whole procedure: the scheme is registered by the
|
||||
app, so following the link hands it the token directly. Copying a 200-character string
|
||||
between two devices is the step that goes wrong, and it does not have to happen at all. -->
|
||||
<p><a class="btn" href="{{.}}">Open in the Echolot app</a></p>
|
||||
{{with $.LinkHref}}<p><a class="btn" href="{{.}}">Open in the Echolot app</a></p>{{end}}
|
||||
<p class="muted">Only works on the phone you are enrolling. From anywhere else, copy the link
|
||||
into the app's enrolment field, or deliver it over adb.</p>
|
||||
<p><code>{{.}}</code></p>
|
||||
|
||||
Reference in New Issue
Block a user