Skip to content

Commit 9556eae

Browse files
Merge pull request #370 from ava-labs/flare-sec-audit
Address Flare Security Audit Findings
2 parents 91dd0b7 + 7cb02c8 commit 9556eae

File tree

40 files changed

+502
-353
lines changed

40 files changed

+502
-353
lines changed

api/admin/performance.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os"
99
"runtime"
1010
"runtime/pprof"
11+
12+
"github.com/ava-labs/avalanchego/utils/perms"
1113
)
1214

1315
const (
@@ -47,7 +49,7 @@ func (p *Performance) StartCPUProfiler() error {
4749
return errCPUProfilerRunning
4850
}
4951

50-
file, err := os.Create(p.cpuProfileName)
52+
file, err := perms.Create(p.cpuProfileName, perms.ReadWrite)
5153
if err != nil {
5254
return err
5355
}
@@ -75,7 +77,7 @@ func (p *Performance) StopCPUProfiler() error {
7577

7678
// MemoryProfile dumps the current memory utilization of this node
7779
func (p *Performance) MemoryProfile() error {
78-
file, err := os.Create(p.memProfileName)
80+
file, err := perms.Create(p.memProfileName, perms.ReadWrite)
7981
if err != nil {
8082
return err
8183
}
@@ -89,7 +91,7 @@ func (p *Performance) MemoryProfile() error {
8991

9092
// LockProfile dumps the current lock statistics of this node
9193
func (p *Performance) LockProfile() error {
92-
file, err := os.Create(p.lockProfileName)
94+
file, err := perms.Create(p.lockProfileName, perms.ReadWrite)
9395
if err != nil {
9496
return err
9597
}

api/admin/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package admin
55

66
import (
77
"errors"
8-
"io/ioutil"
98
"net/http"
109

1110
"github.com/gorilla/rpc/v2"
@@ -15,6 +14,7 @@ import (
1514
"github.com/ava-labs/avalanchego/ids"
1615
"github.com/ava-labs/avalanchego/snow/engine/common"
1716
"github.com/ava-labs/avalanchego/utils/logging"
17+
"github.com/ava-labs/avalanchego/utils/perms"
1818

1919
cjson "github.com/ava-labs/avalanchego/utils/json"
2020
)
@@ -159,5 +159,5 @@ func (service *Admin) Stacktrace(_ *http.Request, _ *struct{}, reply *api.Succes
159159

160160
reply.Success = true
161161
stacktrace := []byte(logging.Stacktrace{Global: true}.String())
162-
return ioutil.WriteFile(stacktraceFile, stacktrace, 0600)
162+
return perms.WriteFile(stacktraceFile, stacktrace, perms.ReadWrite)
163163
}

api/auth/auth.go

Lines changed: 105 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package auth
22

33
import (
4+
"crypto/rand"
5+
"encoding/base64"
46
"encoding/json"
57
"errors"
68
"fmt"
@@ -23,6 +25,9 @@ const (
2325

2426
headerKey = "Authorization"
2527
headerValStart = "Bearer "
28+
29+
// number of bytes to use when generating a new random token ID
30+
tokenIDByteLen = 20
2631
)
2732

2833
var (
@@ -36,23 +41,37 @@ var (
3641
headerKey,
3742
headerValStart,
3843
)
39-
ErrTokenExpired = errors.New("the provided auth token was expired")
44+
ErrInvalidSigningMethod = fmt.Errorf("auth token didn't specify the HS256 signing method correctly")
4045
ErrTokenRevoked = errors.New("the provided auth token was revoked")
4146
ErrTokenInsufficientPermission = errors.New("the provided auth token does not allow access to this endpoint")
4247

43-
errWrongPassword = errors.New("incorrect password")
44-
errInvalidTokenFormat = errors.New("token is invalid format")
45-
errSamePassword = errors.New("new password can't be same as old password")
48+
errWrongPassword = errors.New("incorrect password")
49+
errSamePassword = errors.New("new password can't be same as old password")
4650
)
4751

4852
// Auth handles HTTP API authorization for this node
4953
type Auth struct {
50-
Enabled bool // True iff API calls need auth token
51-
Password password.Hash // Hash of the password. Can be changed via API call.
54+
lock sync.RWMutex // Prevent race condition when accessing password
55+
enabled bool // True iff API calls need auth token
56+
password password.Hash // Hash of the password. Can be changed via API call.
57+
clock timer.Clock // Tells the time. Can be faked for testing
58+
revoked map[string]struct{} // Set of token IDs that have been revoked
59+
}
5260

53-
lock sync.RWMutex // Prevent race condition when accessing password
54-
clock timer.Clock // Tells the time. Can be faked for testing
55-
revoked []string // List of tokens that have been revoked
61+
func New(enabled bool, password string) (*Auth, error) {
62+
auth := &Auth{
63+
enabled: enabled,
64+
revoked: make(map[string]struct{}),
65+
}
66+
return auth, auth.password.Set(password)
67+
}
68+
69+
func NewFromHash(enabled bool, password password.Hash) *Auth {
70+
return &Auth{
71+
enabled: enabled,
72+
password: password,
73+
revoked: make(map[string]struct{}),
74+
}
5675
}
5776

5877
// Custom claim type used for API access token
@@ -62,26 +81,15 @@ type endpointClaims struct {
6281
// Each element is an endpoint that the token allows access to
6382
// If endpoints has an element "*", allows access to all API endpoints
6483
// In this case, "*" should be the only element of [endpoints]
65-
Endpoints []string
84+
Endpoints []string `json:"endpoints,omitempty"`
6685
}
6786

6887
// getTokenKey returns the key to use when making and parsing tokens
69-
func (auth *Auth) getTokenKey(*jwt.Token) (interface{}, error) {
70-
return auth.Password.Password[:], nil
71-
}
72-
73-
// getToken gets the JWT token from the request header
74-
// Assumes the header is this form:
75-
// "Authorization": "Bearer TOKEN.GOES.HERE"
76-
func getToken(r *http.Request) (string, error) {
77-
rawHeader := r.Header.Get(headerKey) // Should be "Bearer AUTH.TOKEN.HERE"
78-
if rawHeader == "" {
79-
return "", ErrNoToken
80-
}
81-
if !strings.HasPrefix(rawHeader, headerValStart) {
82-
return "", errInvalidTokenFormat
88+
func (auth *Auth) getTokenKey(t *jwt.Token) (interface{}, error) {
89+
if t.Method != jwt.SigningMethodHS256 {
90+
return nil, ErrInvalidSigningMethod
8391
}
84-
return rawHeader[len(headerValStart):], nil // Returns actual auth token. Slice guaranteed to not go OOB
92+
return auth.password.Password[:], nil
8593
}
8694

8795
// Create and return a new token that allows access to each API endpoint such
@@ -90,29 +98,38 @@ func getToken(r *http.Request) (string, error) {
9098
func (auth *Auth) newToken(password string, endpoints []string) (string, error) {
9199
auth.lock.RLock()
92100
defer auth.lock.RUnlock()
93-
if !auth.Password.Check(password) {
101+
102+
if !auth.password.Check(password) {
94103
return "", errWrongPassword
95104
}
105+
96106
canAccessAll := false
97107
for _, endpoint := range endpoints {
98108
if endpoint == "*" {
99109
canAccessAll = true
100110
break
101111
}
102112
}
113+
114+
idBytes := [tokenIDByteLen]byte{}
115+
if _, err := rand.Read(idBytes[:]); err != nil {
116+
return "", fmt.Errorf("failed to generate the unique token ID due to %w", err)
117+
}
118+
id := base64.URLEncoding.EncodeToString(idBytes[:])
119+
103120
claims := endpointClaims{
104121
StandardClaims: jwt.StandardClaims{
105122
ExpiresAt: auth.clock.Time().Add(TokenLifespan).Unix(),
123+
Id: id,
106124
},
107125
}
108126
if canAccessAll {
109127
claims.Endpoints = []string{"*"}
110128
} else {
111129
claims.Endpoints = endpoints
112130
}
113-
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
114-
return token.SignedString(auth.Password.Password[:]) // Sign the token and return its string repr.
115-
131+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &claims)
132+
return token.SignedString(auth.password.Password[:]) // Sign the token and return its string repr.
116133
}
117134

118135
// Revokes the token whose string repr. is [tokenStr]; it will not be accepted as authorization for future API calls.
@@ -124,23 +141,61 @@ func (auth *Auth) newToken(password string, endpoints []string) (string, error)
124141
func (auth *Auth) revokeToken(tokenStr string, password string) error {
125142
auth.lock.Lock()
126143
defer auth.lock.Unlock()
127-
if !auth.Password.Check(password) {
144+
145+
if !auth.password.Check(password) {
128146
return errWrongPassword
129147
}
130148

131149
// See if token is well-formed and signature is right
132-
token, err := jwt.Parse(tokenStr, auth.getTokenKey)
150+
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, auth.getTokenKey)
133151
if err != nil {
134152
return err
135153
}
136154

137-
// Only need to revoke if the token is valid
138-
if token.Valid {
139-
auth.revoked = append(auth.revoked, tokenStr)
155+
// If the token isn't valid, it has essentially already been revoked.
156+
if !token.Valid {
157+
return nil
158+
}
159+
160+
claims, ok := token.Claims.(*endpointClaims)
161+
if !ok {
162+
return fmt.Errorf("expected auth token's claims to be type endpointClaims but is %T", token.Claims)
140163
}
164+
auth.revoked[claims.Id] = struct{}{}
141165
return nil
142166
}
143167

168+
// Authenticates [tokenStr] for access to [url].
169+
func (auth *Auth) authenticateToken(tokenStr, url string) error {
170+
auth.lock.RLock()
171+
defer auth.lock.RUnlock()
172+
173+
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, auth.getTokenKey)
174+
if err != nil { // Probably because signature wrong
175+
return err
176+
}
177+
178+
// Make sure this token gives access to the requested endpoint
179+
claims, ok := token.Claims.(*endpointClaims)
180+
if !ok {
181+
// Error is intentionally dropped here as there is nothing left to do
182+
// with it.
183+
return fmt.Errorf("expected auth token's claims to be type endpointClaims but is %T", token.Claims)
184+
}
185+
186+
_, revoked := auth.revoked[claims.Id]
187+
if revoked {
188+
return ErrTokenRevoked
189+
}
190+
191+
for _, endpoint := range claims.Endpoints {
192+
if endpoint == "*" || strings.HasSuffix(url, endpoint) {
193+
return nil
194+
}
195+
}
196+
return ErrTokenInsufficientPermission
197+
}
198+
144199
// Change the password required to create and revoke tokens.
145200
// [oldPassword] is the current password.
146201
// [newPassword] is the new password. It can't be the empty string and it can't
@@ -154,100 +209,56 @@ func (auth *Auth) changePassword(oldPassword, newPassword string) error {
154209
auth.lock.Lock()
155210
defer auth.lock.Unlock()
156211

157-
if !auth.Password.Check(oldPassword) {
212+
if !auth.password.Check(oldPassword) {
158213
return errWrongPassword
159214
}
160215
if err := password.IsValid(newPassword, password.OK); err != nil {
161216
return err
162217
}
163-
if err := auth.Password.Set(newPassword); err != nil {
218+
if err := auth.password.Set(newPassword); err != nil {
164219
return err
165220
}
166221

167222
// All the revoked tokens are now invalid; no need to mark specifically as
168223
// revoked.
169-
auth.revoked = nil
224+
auth.revoked = make(map[string]struct{})
170225
return nil
171226
}
172227

173228
// WrapHandler wraps a handler. Before passing a request to the handler, check that
174229
// an auth token was provided (if necessary) and that it is valid/unexpired.
175230
func (auth *Auth) WrapHandler(h http.Handler) http.Handler {
176-
if !auth.Enabled { // Auth tokens aren't in use. Do nothing.
231+
if !auth.enabled { // Auth tokens aren't in use. Do nothing.
177232
return h
178233
}
179234
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
180-
if path.Base(r.URL.Path) == Endpoint { // Don't require auth token to hit auth endpoint
235+
// Don't require auth token to hit auth endpoint
236+
if path.Base(r.URL.Path) == Endpoint {
181237
h.ServeHTTP(w, r)
182238
return
183239
}
184240

185-
tokenStr, err := getToken(r) // Get the token from the header
186-
if err == ErrNoToken {
187-
// Error is intentionally dropped here as there is nothing left to
188-
// do with it.
189-
writeUnauthorizedResponse(w, err)
190-
return
191-
} else if err != nil {
192-
// Error is intentionally dropped here as there is nothing left to
193-
// do with it.
194-
writeUnauthorizedResponse(w, ErrAuthHeaderNotParsable)
241+
// Should be "Bearer AUTH.TOKEN.HERE"
242+
rawHeader := r.Header.Get(headerKey)
243+
if rawHeader == "" {
244+
writeUnauthorizedResponse(w, ErrNoToken)
195245
return
196246
}
197-
198-
auth.lock.RLock()
199-
token, err := jwt.ParseWithClaims(tokenStr, &endpointClaims{}, auth.getTokenKey)
200-
auth.lock.RUnlock()
201-
202-
if err != nil { // Probably because signature wrong
247+
if !strings.HasPrefix(rawHeader, headerValStart) {
203248
// Error is intentionally dropped here as there is nothing left to
204249
// do with it.
205-
206-
if strings.Contains(err.Error(), "expired") {
207-
writeUnauthorizedResponse(w, ErrTokenExpired)
208-
return
209-
}
210-
211-
writeUnauthorizedResponse(w, fmt.Errorf("invalid auth token: %s", err))
250+
writeUnauthorizedResponse(w, ErrAuthHeaderNotParsable)
212251
return
213252
}
253+
// Returns actual auth token. Slice guaranteed to not go OOB
254+
tokenStr := rawHeader[len(headerValStart):]
214255

215-
// Make sure this token gives access to the requested endpoint
216-
claims, ok := token.Claims.(*endpointClaims)
217-
if !ok {
218-
// Error is intentionally dropped here as there is nothing left to
219-
// do with it.
220-
err := fmt.Errorf("expected auth token's claims to be type endpointClaims but is %T", token.Claims)
256+
if err := auth.authenticateToken(tokenStr, r.URL.Path); err != nil {
221257
writeUnauthorizedResponse(w, err)
222258
return
223259
}
224-
canAccess := false // true iff the token authorizes access to the API
225-
for _, endpoint := range claims.Endpoints {
226-
if endpoint == "*" || strings.HasSuffix(r.URL.Path, endpoint) {
227-
canAccess = true
228-
break
229-
}
230-
}
231-
if !canAccess {
232-
// Error is intentionally dropped here as there is nothing left to
233-
// do with it.
234-
writeUnauthorizedResponse(w, ErrTokenInsufficientPermission)
235-
return
236-
}
237-
238-
auth.lock.RLock()
239-
for _, revokedToken := range auth.revoked { // Make sure this token wasn't revoked
240-
if revokedToken == tokenStr {
241-
// Error is intentionally dropped here as there is nothing left
242-
// to do with it.
243-
writeUnauthorizedResponse(w, ErrTokenRevoked)
244-
auth.lock.RUnlock()
245-
return
246-
}
247-
}
248-
auth.lock.RUnlock()
249260

250-
h.ServeHTTP(w, r) // Authorization successful
261+
h.ServeHTTP(w, r)
251262
})
252263
}
253264

0 commit comments

Comments
 (0)