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:
mrambossek
2026-08-02 08:04:56 +02:00
co-authored by Claude Opus 5
parent f6e093944c
commit 33799b8135
2 changed files with 16 additions and 2 deletions
+15 -1
View File
@@ -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,
})
}