ThatAIguyA world of ideas

THE CONSCIOUSNESS ATLAS
Read. Experiment. Follow the evidence.

ConsciousnessLearn · Explore · Joseph Anady

Janady13/missionmatch / internal/platform/mailer/mailer.go

Source identity, revision, and recovered contents.

GitHub sourceSource text recovered

Source identity

FieldRecorded value
Source typeGitHub source
Original recordJanady13/missionmatch / internal/platform/mailer/mailer.go
DateNot specified in source
RepositoryJanady13/missionmatch
Revision720d47fae476b0d38477ae4401697d372b1ea2bc
Branchfeature/opportunity-to-outcome
Path or archive memberinternal/platform/mailer/mailer.go
Original SHA-256e516f615f325df9fc4f148d619ad892d8855a27bc58fe4ed2de77a621235a828
Git blob SHA52237014a1e56163462176b006a6cc7478e649ce
Reading-copy SHA-256e516f615f325df9fc4f148d619ad892d8855a27bc58fe4ed2de77a621235a828

Original source ↗

Recovered source transcript

// Package mailer sends mail through an ORGANIZATION'S OWN mail server, using credentials that organization // supplied. That single sentence is the whole security problem: an authenticated customer is handing this // server a host name and asking it to open a connection, which is a server side request forgery primitive // unless it is bounded. Everything defensive in this file exists for that reason. // // THE FOUR BOUNDS. // // 1. THE DESTINATION IS RESOLVED AND CHECKED BEFORE ANYTHING IS DIALLED, and the connection is then made to // the address that was checked, not to the name. Checking the name and dialling the name leaves a window // where DNS answers differently the second time, which is the classic rebinding bypass. Loopback, private // ranges, link local and the cloud metadata address are all refused, so this cannot be pointed at the // inside of the network it runs in. // 2. TRANSPORT SECURITY IS MANDATORY. There is no plaintext mode at any port. What travels here is an // activation credential and a person's name. // 3. AUTHENTICATION IS PLAIN OVER TLS ONLY. CRAM MD5, which net/smtp also offers, is MD5 and sits outside // the FIPS validated boundary this platform is built inside. PlainAuth additionally refuses to hand // credentials to a server that has not established TLS, which is a second lock on the first bound. // 4. EVERY HEADER VALUE IS CHECKED FOR CARRIAGE RETURN AND LINE FEED. A display name carrying a newline is // how a header injection becomes a blind carbon copy to an attacker. package mailer import ( "context" "crypto/tls" "errors" "fmt" "net" "net/smtp" "strconv" "github.com/missionworkforce/missionmatch/internal/platform/storage" "strings" "time" ) // Config is one organization's transport, assembled by the caller from settings plus the opened secret. The // password arrives as a value and is never held anywhere in this package beyond the call. type Config struct { Host string Port int Username string Password string Security string // starttls or implicit_tls From string FromName string ReplyTo string } // Message is what to send. Body is plain text, deliberately: an invite is four lines and a link, and an HTML // mail path would mean escaping user supplied names into markup for no gain a customer would notice. type Message struct { To string Subject string Body string // Calendar is an optional RFC 5545 object. When present the message is sent as multipart/alternative // with a text/calendar part, which is how every mainstream calendar client recognises an invitation. // // IT IS ALTERNATIVE, NOT MIXED, and the distinction is the whole reason invitations work. multipart/mixed // produces a message with a file attached, which a calendar ignores and a person has to open by hand; // multipart/alternative with method=REQUEST is what makes the accept and decline buttons appear. Calendar string // CalendarMethod is REQUEST for a new or changed meeting and CANCEL to withdraw one. It must agree with // the METHOD inside the object, or clients disagree about what the message means. CalendarMethod string } var ( ErrBlockedHost = errors.New("mailer: that mail host resolves to an address inside this network and is refused") ErrHeader = errors.New("mailer: a mail header value contained a line break and was refused") ) // dialTimeout bounds the whole exchange. A customer typing a host that silently drops packets must not hold a // request handler open, so this is short enough that a person watching a settings screen still gets an answer. const dialTimeout = 12 * time.Second // Send delivers one message. It returns a transport error suitable for showing to an administrator, having // been scrubbed of credentials by the caller before it is stored. func Send(ctx context.Context, cfg Config, msg Message) error { for _, v := range []string{cfg.From, cfg.FromName, cfg.ReplyTo, msg.To, msg.Subject} { if strings.ContainsAny(v, "\r\n") { return ErrHeader } } if cfg.Host == "" || cfg.Port <= 0 { return errors.New("mailer: no mail host is configured") } if cfg.From == "" { return errors.New("mailer: no sender address is configured") } ip, err := resolveAllowed(ctx, cfg.Host) if err != nil { return err } addr := net.JoinHostPort(ip.String(), strconv.Itoa(cfg.Port)) // ServerName is the NAME the customer configured, never the resolved address, so certificate validation // still checks what it is supposed to check. MinVersion is pinned rather than left to the default so this // stays explicit under review. tlsCfg := &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12} d := &net.Dialer{Timeout: dialTimeout} var conn net.Conn if cfg.Security == "implicit_tls" { conn, err = tls.DialWithDialer(d, "tcp", addr, tlsCfg) } else { conn, err = d.DialContext(ctx, "tcp", addr) } if err != nil { return fmt.Errorf("mailer: connect to %s: %w", cfg.Host, err) } defer conn.Close() if dl, ok := ctx.Deadline(); ok { _ = conn.SetDeadline(dl) } else { _ = conn.SetDeadline(time.Now().Add(dialTimeout * 3)) } c, err := smtp.NewClient(conn, cfg.Host) if err != nil { return fmt.Errorf("mailer: greet %s: %w", cfg.Host, err) } defer func() { _ = c.Quit() }() if cfg.Security != "implicit_tls" { ok, _ := c.Extension("STARTTLS") if !ok { // FAILS CLOSED. A server that will not upgrade does not get the credentials, and the operator is // told exactly why rather than being left with a generic authentication failure. return fmt.Errorf("mailer: %s does not offer STARTTLS and this platform will not send mail in the clear", cfg.Host) } if err := c.StartTLS(tlsCfg); err != nil { return fmt.Errorf("mailer: start TLS with %s: %w", cfg.Host, err) } } if cfg.Username != "" { // PlainAuth carries its own refusal to authenticate over an unencrypted link, so this is the second // independent guard on the same property rather than a duplicate of the first. if err := c.Auth(smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)); err != nil { return fmt.Errorf("mailer: authenticate to %s: %w", cfg.Host, err) } } if err := c.Mail(cfg.From); err != nil { return fmt.Errorf("mailer: sender rejected: %w", err) } if err := c.Rcpt(msg.To); err != nil { return fmt.Errorf("mailer: recipient rejected: %w", err) } wc, err := c.Data() if err != nil { return fmt.Errorf("mailer: begin message: %w", err) } if _, err := wc.Write([]byte(compose(cfg, msg))); err != nil { _ = wc.Close() return fmt.Errorf("mailer: write message: %w", err) } if err := wc.Close(); err != nil { return fmt.Errorf("mailer: deliver message: %w", err) } return nil } func compose(cfg Config, msg Message) string { var b strings.Builder from := cfg.From if cfg.FromName != "" { from = fmt.Sprintf("%q <%s>", cfg.FromName, cfg.From) } b.WriteString("From: " + from + "\r\n") b.WriteString("To: " + msg.To + "\r\n") if cfg.ReplyTo != "" { b.WriteString("Reply-To: " + cfg.ReplyTo + "\r\n") } b.WriteString("Subject: " + msg.Subject + "\r\n") b.WriteString("MIME-Version: 1.0\r\n") if msg.Calendar != "" { method := msg.CalendarMethod if method == "" { method = "REQUEST" } // A fixed boundary would be a bug the day a body legitimately contained it. This one is derived from // the content, so it cannot collide with the content. boundary := "mmboundary" + storage.SHA256Hex([]byte(msg.Subject + msg.To))[:24] b.WriteString("Content-Type: multipart/alternative; boundary=\"" + boundary + "\"\r\n") b.WriteString("Auto-Submitted: auto-generated\r\n") b.WriteString("\r\n") b.WriteString("--" + boundary + "\r\n") b.WriteString("Content-Type: text/plain; charset=utf-8\r\n\r\n") b.WriteString(dotStuff(msg.Body)) b.WriteString("\r\n--" + boundary + "\r\n") b.WriteString("Content-Type: text/calendar; charset=utf-8; method=" + method + "\r\n") b.WriteString("Content-Transfer-Encoding: 7bit\r\n\r\n") b.WriteString(dotStuff(msg.Calendar)) b.WriteString("\r\n--" + boundary + "--\r\n") return b.String() } b.WriteString("Content-Type: text/plain; charset=utf-8\r\n") // Auto-Submitted marks this as machine generated so a recipient's out of office does not answer it, and // so mailing list software does not treat it as human correspondence. RFC 3834. b.WriteString("Auto-Submitted: auto-generated\r\n") b.WriteString("\r\n") b.WriteString(dotStuff(msg.Body)) b.WriteString("\r\n") return b.String() } // dotStuff escapes a line beginning with a single dot, which would otherwise terminate the DATA phase early // and truncate the message at exactly the point an attacker chooses. net/smtp does not do this for us, and a // calendar object is full of lines that a fold could start with anything. func dotStuff(s string) string { return strings.ReplaceAll(s, "\n.", "\n..") } // resolveAllowed turns a host name into ONE address that is safe to dial, or refuses. It returns the first // allowed address rather than all of them because the connection is made to exactly what was checked. func resolveAllowed(ctx context.Context, host string) (net.IP, error) { // A literal address skips resolution but is checked identically. Passing an IP directly must not be a way // around the guard. if ip := net.ParseIP(host); ip != nil { if blockedIP(ip) { return nil, fmt.Errorf("%w: %s", ErrBlockedHost, host) } return ip, nil } rctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() ips, err := net.DefaultResolver.LookupIP(rctx, "ip", host) if err != nil { return nil, fmt.Errorf("mailer: resolve %s: %w", host, err) } for _, ip := range ips { if !blockedIP(ip) { return ip, nil } } return nil, fmt.Errorf("%w: %s", ErrBlockedHost, host) } // blockedIP is the reachability boundary. Everything here is an address that a mail server has no business // living at and that an attacker would very much like this process to connect to on their behalf. func blockedIP(ip net.IP) bool { if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsInterfaceLocalMulticast() || ip.IsMulticast() { return true } if v4 := ip.To4(); v4 != nil { switch { // The cloud instance metadata address. It is link local and therefore already caught above; it is // named separately because it is the single most valuable target of this class of bug and a future // edit to the line above must not silently drop it. case v4[0] == 169 && v4[1] == 254: return true // Carrier grade NAT, 100.64.0.0/10. Not private by the standard library's definition, and it is where // this operator's own machines live. case v4[0] == 100 && v4[1] >= 64 && v4[1] <= 127: return true // Reserved, 240.0.0.0/4, and the broadcast address. case v4[0] >= 240: return true } return false } // IPv6 unique local addresses, fc00::/7, the equivalent of a private range. return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc }

Search equations, builds, manuscript text, and recovered sources.