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.
This commit is contained in:
Anders Pitman
2021-12-18 17:40:59 -07:00
parent 2d4465f665
commit 5db952a069
3 changed files with 92 additions and 54 deletions

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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