Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions bridge/gitlab/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ var (
ErrBadProjectURL = errors.New("bad project url")
)

var (
// Tokens issued before GitLab's routable tokens existed: an optional
// "glpat-" prefix followed by exactly 20 base62 characters.
legacyTokenRe = regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)
// Routable tokens, as generated by GitLab since 16.10:
// "glpat-" followed by a base64url payload (27 to 300 characters), a ".",
// a two character token version, another ".", then the payload length and
// a crc32, both encoded in base36 on two and seven characters.
// See https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/cells/routable_tokens
routableTokenRe = regexp.MustCompile(`^glpat-[0-9a-zA-Z_-]{27,300}\.[0-9a-z]{2}\.[0-9a-z]{2}[0-9a-z]{7}$`)
)

// validTokenFormat tells whether a token looks like a personal access token
// that GitLab could have issued. The actual validity is checked by the API
// call in promptToken, this is only there to catch typos early.
func validTokenFormat(token string) bool {
return legacyTokenRe.MatchString(token) || routableTokenRe.MatchString(token)
}

func (g *Gitlab) ValidParams() map[string]interface{} {
return map[string]interface{}{
"URL": nil,
Expand Down Expand Up @@ -200,12 +219,10 @@ func promptToken(baseUrl string) (*auth.Token, error) {
fmt.Println("'api' access scope: to be able to make api calls")
fmt.Println()

re := regexp.MustCompile(`^(glpat-)?[a-zA-Z0-9\-\_]{20}$`)

var login string

validator := func(name string, value string) (complaint string, err error) {
if !re.MatchString(value) {
if !validTokenFormat(value) {
return "token has incorrect format", nil
}
login, err = getLoginFromToken(baseUrl, auth.NewToken(target, value))
Expand Down
97 changes: 97 additions & 0 deletions bridge/gitlab/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gitlab

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -88,3 +89,99 @@ func TestProjectPath(t *testing.T) {
})
}
}

func TestValidTokenFormat(t *testing.T) {
tests := []struct {
name string
token string
want bool
}{
{
name: "legacy token without prefix",
token: "token-string-here123",
want: true,
},
{
name: "legacy token with prefix",
token: "glpat-token-string-here123",
want: true,
},
{
// https://github.com/trufflesecurity/trufflehog/issues/4551, as
// issued by both gitlab.com and a self-managed instance
name: "routable token",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqjnm",
want: true,
},
{
// a payload close to the 300 characters maximum
name: "routable token with a long payload",
token: "glpat-" + strings.Repeat("a", 300) + ".01.6z70tqjnm",
want: true,
},
{
// the token from the issue report
name: "routable token from the issue report",
token: "glpat-bE8-c60h8AD4A0v-59rAr246MQj8ObF1DA.01.0y0nl1q64",
want: true,
},
{
name: "empty token",
token: "",
want: false,
},
{
name: "too short",
token: "glpat-tooshort",
want: false,
},
{
name: "legacy length with a dot",
token: "glpat-token-string-here1.xxxxxxxxxxxxxxx",
want: false,
},
{
// the payload is 26 characters, one short of the 27 minimum
name: "routable payload too short",
token: "glpat-" + strings.Repeat("a", 26) + ".01.6z70tqjnm",
want: false,
},
{
// the payload is one character over the 300 maximum
name: "routable payload too long",
token: "glpat-" + strings.Repeat("a", 301) + ".01.6z70tqjnm",
want: false,
},
{
name: "routable token without the checksum",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01",
want: false,
},
{
name: "routable token with a bad checksum length",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqj",
want: false,
},
{
name: "routable token with a non base36 checksum",
token: "glpat-rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqj-m",
want: false,
},
{
name: "routable token without a prefix",
token: "rQxN4XPLm8wF_jGbHv_A9731YZq2Kfe0BD.01.6z70tqjnm",
want: false,
},
{
name: "not a token at all",
token: "hunter2",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, validTokenFormat(tt.token))
})
}
}