diff --git a/routers/web/repo/attachment.go b/routers/web/repo/attachment.go index 9eda926dad823..b2fd182bb1e57 100644 --- a/routers/web/repo/attachment.go +++ b/routers/web/repo/attachment.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/services/attachment" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context/upload" + "code.gitea.io/gitea/services/mailer/token" repo_service "code.gitea.io/gitea/services/repository" ) @@ -87,16 +88,59 @@ func DeleteAttachment(ctx *context.Context) { }) } +func checkSecurityLink(ctx *context.Context, uuid string) (*repo_model.Attachment, func()) { + handlerType, user, payload, err := token.ExtractToken(ctx, uuid) + if err != nil { + return nil, nil + } + if handlerType != token.ReadAttachmentHandlerType { + return nil, nil + } + + var attachID int64 + err = util.UnpackData(payload, &attachID) + if err != nil { + return nil, nil + } + + attach, err := repo_model.GetAttachmentByID(ctx, attachID) + if err != nil { + return nil, nil + } + + cUser := ctx.Doer + cIsSigned := ctx.IsSigned + + cancel := func() { + ctx.Doer = cUser + ctx.IsSigned = cIsSigned + } + + ctx.Doer = user + ctx.IsSigned = true + + return attach, cancel +} + // GetAttachment serve attachments with the given UUID func ServeAttachment(ctx *context.Context, uuid string) { - attach, err := repo_model.GetAttachmentByUUID(ctx, uuid) - if err != nil { - if repo_model.IsErrAttachmentNotExist(err) { - ctx.HTTPError(http.StatusNotFound) - } else { - ctx.ServerError("GetAttachmentByUUID", err) + var err error + + attach, cancel := checkSecurityLink(ctx, uuid) + if cancel != nil { + defer cancel() + } + + if attach == nil { + attach, err = repo_model.GetAttachmentByUUID(ctx, uuid) + if err != nil { + if repo_model.IsErrAttachmentNotExist(err) { + ctx.HTTPError(http.StatusNotFound) + } else { + ctx.ServerError("GetAttachmentByUUID", err) + } + return } - return } repository, unitType, err := repo_service.LinkedRepository(ctx, attach) diff --git a/routers/web/web.go b/routers/web/web.go index 01dc8cf697c5b..288f513678307 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1380,7 +1380,7 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments m.Get("/attachments/{uuid}", repo.GetAttachment) - }, optSignIn, context.RepoAssignment) + }, optSignIn) // end "/{username}/{reponame}": compatibility with old attachments m.Group("/{username}/{reponame}", func() { diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 52e19bde6f261..1e5ee1f237b17 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -28,9 +28,12 @@ import ( "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/util" incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload" sender_service "code.gitea.io/gitea/services/mailer/sender" "code.gitea.io/gitea/services/mailer/token" + + "golang.org/x/net/html" ) const ( @@ -228,6 +231,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient return nil, err } + loadedBody, err := ParseMailBody(string(body)) + if err != nil { + return nil, err + } + actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) if actName != "new" { @@ -279,12 +287,6 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient mailMeta["Subject"] = subject - var mailBody bytes.Buffer - - if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil { - log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err) - } - // Make sure to compose independent messages to avoid leaking user emails msgID := generateMessageIDForIssue(ctx.Issue, ctx.Comment, ctx.ActionType) reference := generateMessageIDForIssue(ctx.Issue, nil, activities_model.ActionType(0)) @@ -308,6 +310,20 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient msgs := make([]*sender_service.Message, 0, len(recipients)) for _, recipient := range recipients { + var mailBody bytes.Buffer + + bodyStr, err := loadedBody.RenderBody(ctx, recipient) + if err != nil { + log.Error("RenderBody: %v", err) + continue + } + mailMeta["Body"] = template.HTML(bodyStr) + + if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil { + log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err) + continue + } + msg := sender_service.NewMessageFrom( recipient.Email, fromDisplayName(ctx.Doer), @@ -548,3 +564,119 @@ func fromDisplayName(u *user_model.User) string { } return u.GetCompleteName() } + +type LoadedMailBody struct { + doc *html.Node + allLinks []*html.Attribute +} + +func (ml *LoadedMailBody) RenderBody(ctx *mailCommentContext, user *user_model.User) (string, error) { + allLinks := make(map[string]string) + + for _, l := range ml.allLinks { + if v, ok := allLinks[l.Val]; ok { + l.Val = v + continue + } + + securityURI, err := AttachmentSrcToSecurityURI(ctx, l.Val, user) + if err != nil { + continue + } + + allLinks[l.Val] = securityURI + l.Val = securityURI + } + + var buf bytes.Buffer + err := html.Render(&buf, ml.doc) + if err != nil { + log.Error("Failed to render modified HTML: %v", err) + return "", err + } + + return buf.String(), nil +} + +func ParseMailBody(body string) (*LoadedMailBody, error) { + doc, err := html.Parse(strings.NewReader(body)) + if err != nil { + log.Error("Failed to parse HTML body: %v", err) + return nil, err + } + + var processNode func(*html.Node) + allLins := make([]*html.Attribute, 0) + + processNode = func(n *html.Node) { + if n.Type == html.ElementNode { + if n.Data == "img" { + for i, attr := range n.Attr { + if attr.Key != "src" { + continue + } + + if !strings.HasPrefix(attr.Val, setting.AppURL) { // external image + continue + } + + allLins = append(allLins, &n.Attr[i]) + } + } + + if n.Data == "a" { + for i, attr := range n.Attr { + if attr.Key != "href" { + continue + } + + if !strings.HasPrefix(attr.Val, setting.AppURL) { // external link + continue + } + + allLins = append(allLins, &n.Attr[i]) + } + } + } + + for c := n.FirstChild; c != nil; c = c.NextSibling { + processNode(c) + } + } + + processNode(doc) + + return &LoadedMailBody{ + doc: doc, + allLinks: allLins, + }, nil +} + +func AttachmentSrcToSecurityURI(ctx context.Context, attachmentPath string, user *user_model.User) (string, error) { + if !strings.HasPrefix(attachmentPath, setting.AppURL) { // external image + return "", fmt.Errorf("external image") + } + + parts := strings.Split(attachmentPath, "/attachments/") + if len(parts) <= 1 { + return "", fmt.Errorf("invalid attachment path: %s", attachmentPath) + } + + attachmentUUID := parts[len(parts)-1] + attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID) + if err != nil { + return "", err + } + + payload, err := util.PackData(attachment.ID) + if err != nil { + return "", err + } + + key, err := token.CreateToken(token.ReadAttachmentHandlerType, user, payload) + if err != nil { + return "", err + } + + return strings.Replace(attachmentPath, attachmentUUID, key, 1), nil +} diff --git a/services/mailer/token/token.go b/services/mailer/token/token.go index 8a5a762d6b5fd..4fc1416624c7b 100644 --- a/services/mailer/token/token.go +++ b/services/mailer/token/token.go @@ -34,6 +34,7 @@ const ( UnknownHandlerType HandlerType = iota ReplyHandlerType UnsubscribeHandlerType + ReadAttachmentHandlerType ) var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)