Skip to content

Commit 986e0ed

Browse files
committed
make private image in email can be read by security link
follow #32061, but fix it by generate a special link by reuse mail token logic. Signed-off-by: a1012112796 <[email protected]>
1 parent 698ae7a commit 986e0ed

File tree

4 files changed

+191
-14
lines changed

4 files changed

+191
-14
lines changed

routers/web/repo/attachment.go

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"code.gitea.io/gitea/services/attachment"
1919
"code.gitea.io/gitea/services/context"
2020
"code.gitea.io/gitea/services/context/upload"
21+
"code.gitea.io/gitea/services/mailer/token"
2122
repo_service "code.gitea.io/gitea/services/repository"
2223
)
2324

@@ -87,16 +88,59 @@ func DeleteAttachment(ctx *context.Context) {
8788
})
8889
}
8990

91+
func checkSecurityLink(ctx *context.Context, uuid string) (*repo_model.Attachment, func()) {
92+
handlerType, user, payload, err := token.ExtractToken(ctx, uuid)
93+
if err != nil {
94+
return nil, nil
95+
}
96+
if handlerType != token.ReadAttachmentHandlerType {
97+
return nil, nil
98+
}
99+
100+
var attachID int64
101+
err = util.UnpackData(payload, &attachID)
102+
if err != nil {
103+
return nil, nil
104+
}
105+
106+
attach, err := repo_model.GetAttachmentByID(ctx, attachID)
107+
if err != nil {
108+
return nil, nil
109+
}
110+
111+
cUser := ctx.Doer
112+
cIsSigned := ctx.IsSigned
113+
114+
cancel := func() {
115+
ctx.Doer = cUser
116+
ctx.IsSigned = cIsSigned
117+
}
118+
119+
ctx.Doer = user
120+
ctx.IsSigned = true
121+
122+
return attach, cancel
123+
}
124+
90125
// GetAttachment serve attachments with the given UUID
91126
func ServeAttachment(ctx *context.Context, uuid string) {
92-
attach, err := repo_model.GetAttachmentByUUID(ctx, uuid)
93-
if err != nil {
94-
if repo_model.IsErrAttachmentNotExist(err) {
95-
ctx.HTTPError(http.StatusNotFound)
96-
} else {
97-
ctx.ServerError("GetAttachmentByUUID", err)
127+
var err error
128+
129+
attach, cancel := checkSecurityLink(ctx, uuid)
130+
if cancel != nil {
131+
defer cancel()
132+
}
133+
134+
if attach == nil {
135+
attach, err = repo_model.GetAttachmentByUUID(ctx, uuid)
136+
if err != nil {
137+
if repo_model.IsErrAttachmentNotExist(err) {
138+
ctx.HTTPError(http.StatusNotFound)
139+
} else {
140+
ctx.ServerError("GetAttachmentByUUID", err)
141+
}
142+
return
98143
}
99-
return
100144
}
101145

102146
repository, unitType, err := repo_service.LinkedRepository(ctx, attach)

routers/web/web.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1380,7 +1380,7 @@ func registerRoutes(m *web.Router) {
13801380

13811381
m.Group("/{username}/{reponame}", func() { // to maintain compatibility with old attachments
13821382
m.Get("/attachments/{uuid}", repo.GetAttachment)
1383-
}, optSignIn, context.RepoAssignment)
1383+
}, optSignIn)
13841384
// end "/{username}/{reponame}": compatibility with old attachments
13851385

13861386
m.Group("/{username}/{reponame}", func() {

services/mailer/mail.go

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ import (
2828
"code.gitea.io/gitea/modules/templates"
2929
"code.gitea.io/gitea/modules/timeutil"
3030
"code.gitea.io/gitea/modules/translation"
31+
"code.gitea.io/gitea/modules/util"
3132
incoming_payload "code.gitea.io/gitea/services/mailer/incoming/payload"
3233
sender_service "code.gitea.io/gitea/services/mailer/sender"
3334
"code.gitea.io/gitea/services/mailer/token"
35+
36+
"golang.org/x/net/html"
3437
)
3538

3639
const (
@@ -228,6 +231,11 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
228231
return nil, err
229232
}
230233

234+
loadedBody, err := ParseMailBody(string(body))
235+
if err != nil {
236+
return nil, err
237+
}
238+
231239
actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
232240

233241
if actName != "new" {
@@ -279,12 +287,6 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
279287

280288
mailMeta["Subject"] = subject
281289

282-
var mailBody bytes.Buffer
283-
284-
if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
285-
log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err)
286-
}
287-
288290
// Make sure to compose independent messages to avoid leaking user emails
289291
msgID := generateMessageIDForIssue(ctx.Issue, ctx.Comment, ctx.ActionType)
290292
reference := generateMessageIDForIssue(ctx.Issue, nil, activities_model.ActionType(0))
@@ -308,6 +310,20 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
308310

309311
msgs := make([]*sender_service.Message, 0, len(recipients))
310312
for _, recipient := range recipients {
313+
var mailBody bytes.Buffer
314+
315+
bodyStr, err := loadedBody.RenderBody(ctx, recipient)
316+
if err != nil {
317+
log.Error("RenderBody: %v", err)
318+
continue
319+
}
320+
mailMeta["Body"] = template.HTML(bodyStr)
321+
322+
if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
323+
log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err)
324+
continue
325+
}
326+
311327
msg := sender_service.NewMessageFrom(
312328
recipient.Email,
313329
fromDisplayName(ctx.Doer),
@@ -548,3 +564,119 @@ func fromDisplayName(u *user_model.User) string {
548564
}
549565
return u.GetCompleteName()
550566
}
567+
568+
type LoadedMailBody struct {
569+
doc *html.Node
570+
allLinks []*html.Attribute
571+
}
572+
573+
func (ml *LoadedMailBody) RenderBody(ctx *mailCommentContext, user *user_model.User) (string, error) {
574+
allLinks := make(map[string]string)
575+
576+
for _, l := range ml.allLinks {
577+
if v, ok := allLinks[l.Val]; ok {
578+
l.Val = v
579+
continue
580+
}
581+
582+
securityURI, err := AttachmentSrcToSecurityURI(ctx, l.Val, user)
583+
if err != nil {
584+
continue
585+
}
586+
587+
allLinks[l.Val] = securityURI
588+
l.Val = securityURI
589+
}
590+
591+
var buf bytes.Buffer
592+
err := html.Render(&buf, ml.doc)
593+
if err != nil {
594+
log.Error("Failed to render modified HTML: %v", err)
595+
return "", err
596+
}
597+
598+
return buf.String(), nil
599+
}
600+
601+
func ParseMailBody(body string) (*LoadedMailBody, error) {
602+
doc, err := html.Parse(strings.NewReader(body))
603+
if err != nil {
604+
log.Error("Failed to parse HTML body: %v", err)
605+
return nil, err
606+
}
607+
608+
var processNode func(*html.Node)
609+
allLins := make([]*html.Attribute, 0)
610+
611+
processNode = func(n *html.Node) {
612+
if n.Type == html.ElementNode {
613+
if n.Data == "img" {
614+
for i, attr := range n.Attr {
615+
if attr.Key != "src" {
616+
continue
617+
}
618+
619+
if !strings.HasPrefix(attr.Val, setting.AppURL) { // external image
620+
continue
621+
}
622+
623+
allLins = append(allLins, &n.Attr[i])
624+
}
625+
}
626+
627+
if n.Data == "a" {
628+
for i, attr := range n.Attr {
629+
if attr.Key != "href" {
630+
continue
631+
}
632+
633+
if !strings.HasPrefix(attr.Val, setting.AppURL) { // external link
634+
continue
635+
}
636+
637+
allLins = append(allLins, &n.Attr[i])
638+
}
639+
}
640+
}
641+
642+
for c := n.FirstChild; c != nil; c = c.NextSibling {
643+
processNode(c)
644+
}
645+
}
646+
647+
processNode(doc)
648+
649+
return &LoadedMailBody{
650+
doc: doc,
651+
allLinks: allLins,
652+
}, nil
653+
}
654+
655+
func AttachmentSrcToSecurityURI(ctx context.Context, attachmentPath string, user *user_model.User) (string, error) {
656+
if !strings.HasPrefix(attachmentPath, setting.AppURL) { // external image
657+
return "", fmt.Errorf("external image")
658+
}
659+
660+
parts := strings.Split(attachmentPath, "/attachments/")
661+
if len(parts) <= 1 {
662+
return "", fmt.Errorf("invalid attachment path: %s", attachmentPath)
663+
}
664+
665+
attachmentUUID := parts[len(parts)-1]
666+
attachment, err := repo_model.GetAttachmentByUUID(ctx, attachmentUUID)
667+
if err != nil {
668+
return "", err
669+
}
670+
671+
payload, err := util.PackData(attachment.ID)
672+
if err != nil {
673+
return "", err
674+
}
675+
676+
key, err := token.CreateToken(token.ReadAttachmentHandlerType, user, payload)
677+
if err != nil {
678+
return "", err
679+
}
680+
681+
return strings.Replace(attachmentPath, attachmentUUID, key, 1), nil
682+
}

services/mailer/token/token.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
UnknownHandlerType HandlerType = iota
3535
ReplyHandlerType
3636
UnsubscribeHandlerType
37+
ReadAttachmentHandlerType
3738
)
3839

3940
var encodingWithoutPadding = base32.StdEncoding.WithPadding(base32.NoPadding)

0 commit comments

Comments
 (0)