From 5db952a0690cdad0eb3ab4cc1fcaa6f19de6ddc0 Mon Sep 17 00:00:00 2001 From: Anders Pitman Date: Sat, 18 Dec 2021 17:40:59 -0700 Subject: [PATCH] Improve security of TakingNames.io integration The requests themselves now must be retrieve from the boringproxy server by TakingNames.io, over HTTPS. This provides several security benefits: * You can tell the user the request is coming from a specific domain. * Requests are tied to an ephemeral request-id, to prevent prebuilt phishing links. There is currently a single hard-coded exception for setting a single A record for an IP address. This is needed for bootstrapping a service that doesn't have any certs yet (ie the boringproxy admin domain), and will need to display a big scary message to users. --- boringproxy.go | 60 +++++++++++++++++++++++++++++++------------------- database.go | 53 +++++++++++++++++++++++++++++++++++++++----- ui_handler.go | 33 +++++++-------------------- 3 files changed, 92 insertions(+), 54 deletions(-) diff --git a/boringproxy.go b/boringproxy.go index 7b26e47..87295ba 100644 --- a/boringproxy.go +++ b/boringproxy.go @@ -12,7 +12,6 @@ import ( "log" "net" "net/http" - "net/url" "os" "strings" "sync" @@ -212,11 +211,43 @@ func Listen() { timestamp := time.Now().Format(time.RFC3339) srcIp := strings.Split(r.RemoteAddr, ":")[0] fmt.Println(fmt.Sprintf("%s %s %s %s %s", timestamp, srcIp, r.Method, r.Host, r.URL.Path)) - if r.URL.Path == "/domain-callback" { + if r.URL.Path == "/webdo/requests" { r.ParseForm() + requestId := r.Form.Get("request-id") + + dnsRequest, err := db.GetDNSRequest(requestId) + if err != nil { + w.WriteHeader(500) + io.WriteString(w, err.Error()) + return + } + + jsonBytes, err := json.Marshal(dnsRequest) + if err != nil { + w.WriteHeader(500) + io.WriteString(w, err.Error()) + return + } + + w.Write(jsonBytes) + + } else if r.URL.Path == "/webdo/callback" { + r.ParseForm() + + requestId := r.Form.Get("request-id") + + // Ensure the request exists + _, err := db.GetDNSRequest(requestId) + if err != nil { + w.WriteHeader(500) + io.WriteString(w, err.Error()) + return + } + + db.DeleteDNSRequest(requestId) + domain := r.Form.Get("domain") - // TODO: Check request ID http.Redirect(w, r, fmt.Sprintf("https://%s/edit-tunnel?domain=%s", config.WebUiDomain, domain), 303) } else if r.Host == config.WebUiDomain { if strings.HasPrefix(r.URL.Path, "/api/") { @@ -328,24 +359,7 @@ func getAdminDomain(ip string, certConfig *certmagic.Config) string { requestId, _ := genRandomCode(32) - req := &Request{ - RequestId: requestId, - RedirectUri: fmt.Sprintf("http://%s/domain-callback", ip), - Records: []*Record{ - &Record{ - Type: "A", - Value: ip, - TTL: 300, - }, - }, - } - - jsonBytes, err := json.Marshal(req) - if err != nil { - os.Exit(1) - } - - tnLink := "https://takingnames.io/approve?r=" + url.QueryEscape(string(jsonBytes)) + tnLink := fmt.Sprintf("https://takingnames.io/webdo?requester=%s&request-id=%s&request-type=%s", ip, requestId, "set-ip") // Create a temporary web server to handle the callback which contains the domain @@ -356,7 +370,7 @@ func getAdminDomain(ip string, certConfig *certmagic.Config) string { Handler: mux, } - mux.HandleFunc("/domain-callback", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/webdo/callback", func(w http.ResponseWriter, r *http.Request) { r.ParseForm() domain := r.Form.Get("domain") @@ -372,7 +386,7 @@ func getAdminDomain(ip string, certConfig *certmagic.Config) string { adminDomain = domain - err = certConfig.ManageSync([]string{adminDomain}) + err := certConfig.ManageSync([]string{adminDomain}) if err != nil { log.Fatal(err) } diff --git a/database.go b/database.go index 4b1ee66..51bfb76 100644 --- a/database.go +++ b/database.go @@ -9,11 +9,12 @@ import ( ) type Database struct { - AdminDomain string `json:"admin_domain"` - Tokens map[string]TokenData `json:"tokens"` - Tunnels map[string]Tunnel `json:"tunnels"` - Users map[string]User `json:"users"` - SshKeys map[string]SshKey `json:"ssh_keys"` + AdminDomain string `json:"admin_domain"` + Tokens map[string]TokenData `json:"tokens"` + Tunnels map[string]Tunnel `json:"tunnels"` + Users map[string]User `json:"users"` + SshKeys map[string]SshKey `json:"ssh_keys"` + dnsRequests map[string]DNSRequest `json:"dns_requests"` mutex *sync.Mutex } @@ -34,6 +35,16 @@ type SshKey struct { type DbClient struct { } +type DNSRequest struct { + Records []*DNSRecord `json:"records"` +} + +type DNSRecord struct { + Type string `json:"type"` + Value string `json:"value"` + TTL int `json:"ttl"` +} + type Tunnel struct { Owner string `json:"owner"` Domain string `json:"domain"` @@ -86,6 +97,10 @@ func NewDatabase() (*Database, error) { db.SshKeys = make(map[string]SshKey) } + if db.dnsRequests == nil { + db.dnsRequests = make(map[string]DNSRequest) + } + db.mutex = &sync.Mutex{} db.mutex.Lock() @@ -103,7 +118,6 @@ func (d *Database) SetAdminDomain(adminDomain string) { d.persist() } - func (d *Database) GetAdminDomain() string { d.mutex.Lock() defer d.mutex.Unlock() @@ -111,6 +125,33 @@ func (d *Database) GetAdminDomain() string { return d.AdminDomain } +func (d *Database) SetDNSRequest(requestId string, request DNSRequest) { + d.mutex.Lock() + defer d.mutex.Unlock() + + d.dnsRequests[requestId] = request + + // Not currently persisting because dnsRequests is only stored in + // memory. May change in the future. + //d.persist() +} +func (d *Database) GetDNSRequest(requestId string) (DNSRequest, error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + if req, ok := d.dnsRequests[requestId]; ok { + return req, nil + } + + return DNSRequest{}, errors.New("No such DNS Request") +} +func (d *Database) DeleteDNSRequest(requestId string) { + d.mutex.Lock() + defer d.mutex.Unlock() + + delete(d.dnsRequests, requestId) +} + func (d *Database) AddToken(owner string) (string, error) { d.mutex.Lock() defer d.mutex.Unlock() diff --git a/ui_handler.go b/ui_handler.go index 3768fcf..c135608 100644 --- a/ui_handler.go +++ b/ui_handler.go @@ -3,14 +3,14 @@ package boringproxy import ( "embed" "encoding/base64" - "encoding/json" + //"encoding/json" "fmt" qrcode "github.com/skip2/go-qrcode" "html/template" "io" "net/http" - "net/url" - "os" + //"net/url" + //"os" "strings" "sync" "time" @@ -58,18 +58,6 @@ type LoginData struct { Head template.HTML } -type Request struct { - RequestId string `json:"request_id"` - RedirectUri string `json:"redirect_uri"` - Records []*Record `json:"records:` -} - -type Record struct { - Type string `json:"type"` - Value string `json:"value"` - TTL int `json:"ttl"` -} - func NewWebUiHandler(config *Config, db *Database, api *Api, auth *Auth, tunMan *TunnelManager) *WebUiHandler { return &WebUiHandler{ config: config, @@ -178,11 +166,9 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request requestId, _ := genRandomCode(32) - req := &Request{ - RequestId: requestId, - RedirectUri: fmt.Sprintf("https://%s/domain-callback", h.config.WebUiDomain), - Records: []*Record{ - &Record{ + req := DNSRequest{ + Records: []*DNSRecord{ + &DNSRecord{ Type: "A", Value: h.config.PublicIp, TTL: 300, @@ -190,12 +176,9 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request }, } - jsonBytes, err := json.Marshal(req) - if err != nil { - os.Exit(1) - } + h.db.SetDNSRequest(requestId, req) - tnLink := "https://takingnames.io/approve?r=" + url.QueryEscape(string(jsonBytes)) + tnLink := fmt.Sprintf("https://takingnames.io/webdo?requester=%s&request-id=%s", h.config.WebUiDomain, requestId) templateData := struct { Domain string