add /.well-known/security.txt endpoint

resolves #38
adds RFC 9116 machine parsable
File Format to Aid in Security Vulnerability Disclosure
This commit is contained in:
Alex Syrnikov 2023-06-27 03:43:33 +03:00
parent efc17a6d3c
commit 8ab1f8375c
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) { m.Get("/change-password", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/user/settings/account") ctx.Redirect(setting.AppSubURL + "/user/settings/account")
}) })
m.Get("/security.txt", securityTxt)
}) })
m.Group("/explore", func() { m.Group("/explore", func() {

View file

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