[GITEA] add /.well-known/security.txt endpoint

resolves #38
adds RFC 9116 machine parsable
File Format to Aid in Security Vulnerability Disclosure

(cherry picked from commit 8ab1f8375c)
(cherry picked from commit 8f04f0e288)
(cherry picked from commit 5ced68a7a0)
This commit is contained in:
Alex Syrnikov 2023-06-27 03:43:33 +03:00 committed by Earl Warren
parent 874f07cec2
commit 437c5dd749
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
4 changed files with 83 additions and 0 deletions

View file

@ -0,0 +1,24 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"net/http"
"code.gitea.io/gitea/modules/context"
)
const securityTxtContent = `Contact: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING.md
Contact: mailto:security@forgejo.org
Expires: 2025-06-25T00:00:00Z
Policy: https://codeberg.org/forgejo/forgejo/src/branch/forgejo/CONTRIBUTING.md
Preferred-Languages: en
`
// returns /.well-known/security.txt content
// RFC 9116, https://www.rfc-editor.org/rfc/rfc9116
// https://securitytxt.org/
func securityTxt(ctx *context.Context) {
ctx.PlainText(http.StatusOK, securityTxtContent)
}

View file

@ -0,0 +1,57 @@
// Copyright 2023 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
import (
"regexp"
"testing"
"time"
)
func extractLines(message, pattern string) []string {
ptn := regexp.MustCompile(pattern)
return ptn.FindAllString(message, -1)
}
func TestSecurityTxt(t *testing.T) {
// Contact: is required and value MUST be https:// or mailto:
{
contacts := extractLines(securityTxtContent, `(?m:^Contact: .+$)`)
if contacts == nil {
t.Error("Error: \"Contact: \" field is required")
}
for _, contact := range contacts {
match, err := regexp.MatchString("Contact: (https:)|(mailto:)", contact)
if !match {
t.Error("Error in line ", contact, "\n\"Contact:\" field have incorrect format")
}
if err != nil {
t.Error("Error in line ", contact, err)
}
}
}
// Expires is required
{
expires := extractLines(securityTxtContent, `(?m:^Expires: .+$)`)
if expires == nil {
t.Error("Error: \"Expires: \" field is required")
}
if len(expires) != 1 {
t.Error("Error: \"Expires: \" MUST be single")
}
expRe := regexp.MustCompile(`Expires: (.*)`)
expSlice := expRe.FindStringSubmatch(expires[0])
if len(expSlice) != 2 {
t.Error("Error: \"Expires: \" have no value")
}
expValue := expSlice[1]
expTime, err := time.Parse(time.RFC3339, expValue)
if err != nil {
t.Error("Error parsing Expires value", expValue, err)
}
if time.Now().AddDate(0, 2, 0).After(expTime) {
t.Error("Error: Expires date time almost in the past", expTime)
}
}
}

View file

@ -351,6 +351,7 @@ func registerRoutes(m *web.Route) {
m.Get("/change-password", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
})
m.Get("/security.txt", securityTxt)
})
m.Group("/explore", func() {

View file

@ -38,6 +38,7 @@ func TestLinksNoLogin(t *testing.T) {
"/user2/repo1/projects/1",
"/assets/img/404.png",
"/assets/img/500.png",
"/.well-known/security.txt",
}
for _, link := range links {