mirror of
https://github.com/axllent/mailpit.git
synced 2026-06-27 14:36:07 +00:00
The fix for GHSA-fpxj-m5q8-fphw only capped POST /api/v1/send. Four sibling endpoints (SetReadStatus, DeleteMessages, SetMessageTags, ReleaseMessage) decoded json.NewDecoder(r.Body) with no size limit, allowing an unauthenticated attacker to drive unbounded memory growth via a large IDs array. Apply a 5 MB cap in middleWareFunc so all current and future API handlers inherit it automatically. POST /api/v1/send is exempt via a bodyLimitKey context value set in sendAPIAuthMiddleware, preserving its existing config.MaxMessageSize (default 50 MB) limit. Also fix TestAPIv1SendMaxMessageSize, which was broken by a Go 1.26 change: json.Decoder now wraps reader errors in *json.SyntaxError rather than returning *http.MaxBytesError directly, causing the errors.As check to miss it and return 400 instead of 413. Reading the body with io.ReadAll before decoding surfaces the raw error, restoring correct 413 behaviour on Go 1.25 and 1.26.
204 lines
5.2 KiB
Go
204 lines
5.2 KiB
Go
package apiv1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/mail"
|
|
"strings"
|
|
|
|
"github.com/axllent/mailpit/config"
|
|
"github.com/axllent/mailpit/internal/smtpd"
|
|
"github.com/axllent/mailpit/internal/tools"
|
|
"github.com/jhillyerd/enmime/v2"
|
|
)
|
|
|
|
// SendMessageHandler handles HTTP requests to send a new message
|
|
func SendMessageHandler(w http.ResponseWriter, r *http.Request) {
|
|
// swagger:route POST /api/v1/send message SendMessageParams
|
|
//
|
|
// # Send a message
|
|
//
|
|
// Send a message via the HTTP API.
|
|
//
|
|
// Consumes:
|
|
// - application/json
|
|
//
|
|
// Produces:
|
|
// - application/json
|
|
//
|
|
// Schemes: http, https
|
|
//
|
|
// Responses:
|
|
// 200: SendMessageResponse
|
|
// 400: JSONErrorResponse
|
|
|
|
if config.DemoMode {
|
|
httpJSONError(w, "this functionality has been disabled for demonstration purposes")
|
|
return
|
|
}
|
|
|
|
if config.MaxMessageSize > 0 {
|
|
r.Body = http.MaxBytesReader(w, r.Body, int64(config.MaxMessageSize)*1024*1024)
|
|
}
|
|
|
|
// Read body before decoding so that MaxBytesReader errors are returned directly.
|
|
// In Go 1.26+, json.Decoder wraps reader errors in *json.SyntaxError, which
|
|
// prevents errors.As from finding *http.MaxBytesError to return a 413.
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
var maxErr *http.MaxBytesError
|
|
if errors.As(err, &maxErr) {
|
|
w.WriteHeader(http.StatusRequestEntityTooLarge)
|
|
}
|
|
httpJSONError(w, err.Error())
|
|
return
|
|
}
|
|
|
|
data := sendMessageParams{}
|
|
|
|
if err := json.NewDecoder(bytes.NewReader(body)).Decode(&data.Body); err != nil {
|
|
httpJSONError(w, err.Error())
|
|
return
|
|
}
|
|
|
|
var httpAuthUser *string
|
|
if user, _, ok := r.BasicAuth(); ok {
|
|
httpAuthUser = &user
|
|
}
|
|
|
|
id, err := data.Send(r.RemoteAddr, httpAuthUser)
|
|
|
|
if err != nil {
|
|
httpJSONError(w, err.Error())
|
|
return
|
|
}
|
|
|
|
w.Header().Add("Content-Type", "application/json")
|
|
if err := json.NewEncoder(w).Encode(struct{ ID string }{ID: id}); err != nil {
|
|
httpError(w, err.Error())
|
|
}
|
|
}
|
|
|
|
// Send will validate the message structure and attempt to send to Mailpit.
|
|
// It returns a sending summary or an error.
|
|
func (d sendMessageParams) Send(remoteAddr string, httpAuthUser *string) (string, error) {
|
|
ip, _, err := net.SplitHostPort(remoteAddr)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error parsing request RemoteAddr: %s", err.Error())
|
|
}
|
|
|
|
ipAddr := &net.IPAddr{IP: net.ParseIP(ip)}
|
|
|
|
addresses := []string{}
|
|
|
|
msg := enmime.Builder().
|
|
From(d.Body.From.Name, d.Body.From.Email).
|
|
Subject(d.Body.Subject).
|
|
Text([]byte(d.Body.Text))
|
|
|
|
if d.Body.HTML != "" {
|
|
msg = msg.HTML([]byte(d.Body.HTML))
|
|
}
|
|
|
|
if len(d.Body.To) > 0 {
|
|
for _, a := range d.Body.To {
|
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
|
msg = msg.To(a.Name, a.Email)
|
|
addresses = append(addresses, a.Email)
|
|
} else {
|
|
return "", fmt.Errorf("invalid To address: %s", a.Email)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(d.Body.Cc) > 0 {
|
|
for _, a := range d.Body.Cc {
|
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
|
msg = msg.CC(a.Name, a.Email)
|
|
addresses = append(addresses, a.Email)
|
|
} else {
|
|
return "", fmt.Errorf("invalid Cc address: %s", a.Email)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(d.Body.Bcc) > 0 {
|
|
for _, e := range d.Body.Bcc {
|
|
if _, err := mail.ParseAddress(e); err == nil {
|
|
msg = msg.BCC("", e)
|
|
addresses = append(addresses, e)
|
|
} else {
|
|
return "", fmt.Errorf("invalid Bcc address: %s", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(d.Body.ReplyTo) > 0 {
|
|
for _, a := range d.Body.ReplyTo {
|
|
if _, err := mail.ParseAddress(a.Email); err == nil {
|
|
msg = msg.ReplyTo(a.Name, a.Email)
|
|
} else {
|
|
return "", fmt.Errorf("invalid Reply-To address: %s", a.Email)
|
|
}
|
|
}
|
|
}
|
|
|
|
restrictedHeaders := []string{"To", "From", "Cc", "Bcc", "Reply-To", "Date", "Subject", "Content-Type", "Mime-Version"}
|
|
|
|
if len(d.Body.Tags) > 0 {
|
|
msg = msg.Header("X-Tags", strings.Join(d.Body.Tags, ", "))
|
|
restrictedHeaders = append(restrictedHeaders, "X-Tags")
|
|
}
|
|
|
|
if len(d.Body.Headers) > 0 {
|
|
for k, v := range d.Body.Headers {
|
|
// check header isn't in "restricted" headers
|
|
if tools.InArray(k, restrictedHeaders) {
|
|
return "", fmt.Errorf("cannot overwrite header: \"%s\"", k)
|
|
}
|
|
msg = msg.Header(k, v)
|
|
}
|
|
}
|
|
|
|
if len(d.Body.Attachments) > 0 {
|
|
for _, a := range d.Body.Attachments {
|
|
// workaround: split string because JS readAsDataURL() returns the base64 string
|
|
// with the mime type prefix eg: data:image/png;base64,<base64String>
|
|
parts := strings.Split(a.Content, ",")
|
|
content := parts[len(parts)-1]
|
|
b, err := base64.StdEncoding.DecodeString(content)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error decoding base64 attachment \"%s\": %s", a.Filename, err.Error())
|
|
}
|
|
contentType := http.DetectContentType(b)
|
|
if a.ContentType != "" {
|
|
contentType = a.ContentType
|
|
}
|
|
if a.ContentID != "" {
|
|
msg = msg.AddInline(b, contentType, a.Filename, a.ContentID)
|
|
} else {
|
|
msg = msg.AddAttachment(b, contentType, a.Filename)
|
|
}
|
|
}
|
|
}
|
|
|
|
part, err := msg.Build()
|
|
if err != nil {
|
|
return "", fmt.Errorf("error building message: %s", err.Error())
|
|
}
|
|
|
|
var buff bytes.Buffer
|
|
|
|
if err := part.Encode(io.Writer(&buff)); err != nil {
|
|
return "", fmt.Errorf("error building message: %s", err.Error())
|
|
}
|
|
|
|
return smtpd.SaveToDatabase(ipAddr, d.Body.From.Email, addresses, buff.Bytes(), httpAuthUser)
|
|
}
|