git.iko.soy/typeable/oauth2-proxy
on master help sign in
Address gosec findings

Mostly handling unhandled errors appropriately. If logging to STDERR fails, we panic. Added #nosec comments to findings we are OK with.

commit65c228394fb1a18c824eb472be07b848dd595745 authorNick Meves <nick.meves@greenhouse.io> dateSun 19 Jul 2020, 22:24 (6 years ago) parent7b21f53
16 files changed+155−41
CHANGELOG.md+1 http.go+8−2 main.go+6−2 oauthproxy.go+44−10 pkg/authentication/basic/htpasswd.go+16−8 pkg/encryption/utils.go+14−2 pkg/logger/logger.go+24−6 pkg/middleware/stored_session.go+5−2 pkg/upstream/http.go+2 pkg/upstream/proxy.go+4−1 pkg/util/util.go+1−1 pkg/validation/logging.go+5−2 pkg/validation/options.go+5−1 providers/logingov.go+3−1 validator.go+7−1 watcher.go+10−2
CHANGELOG.md+1
@@ -11,6 +11,7 @@
1111
1212## Changes since v6.0.0
1313
14+- [#690](https://github.com/oauth2-proxy/oauth2-proxy/pull/690) Address GoSec security findings & remediate (@NickMeves)
1415- [#689](https://github.com/oauth2-proxy/oauth2-proxy/pull/689) Fix finicky logging_handler_test from time drift (@NickMeves)
1516- [#699](https://github.com/oauth2-proxy/oauth2-proxy/pull/699) Align persistence ginkgo tests with conventions (@NickMeves)
1617- [#696](https://github.com/oauth2-proxy/oauth2-proxy/pull/696) Preserve query when building redirect
http.go+8−2
@@ -124,7 +124,13 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
124124 if err != nil {
125125 return
126126 }
127− tc.SetKeepAlive(true)
128− tc.SetKeepAlivePeriod(3 * time.Minute)
127+ err = tc.SetKeepAlive(true)
128+ if err != nil {
129+ return nil, err
130+ }
131+ err = tc.SetKeepAlivePeriod(3 * time.Minute)
132+ if err != nil {
133+ return nil, err
134+ }
129135 return tc, nil
130136}
main.go+6−2
@@ -25,7 +25,11 @@ func main() {
2525 config := flagSet.String("config", "", "path to config file")
2626 showVersion := flagSet.Bool("version", false, "print version string")
2727
28− flagSet.Parse(os.Args[1:])
28+ err := flagSet.Parse(os.Args[1:])
29+ if err != nil {
30+ logger.Printf("ERROR: Failed to parse flags: %v", err)
31+ os.Exit(1)
32+ }
2933
3034 if *showVersion {
3135 fmt.Printf("oauth2-proxy %s (built with %s)\n", VERSION, runtime.Version())
@@ -33,7 +37,7 @@ func main() {
3337 }
3438
3539 legacyOpts := options.NewLegacyOptions()
36− err := options.Load(*config, flagSet, legacyOpts)
40+ err = options.Load(*config, flagSet, legacyOpts)
3741 if err != nil {
3842 logger.Printf("ERROR: Failed to load config: %v", err)
3943 os.Exit(1)
oauthproxy.go+44−10
@@ -364,7 +364,11 @@ func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *s
364364// RobotsTxt disallows scraping pages from the OAuthProxy
365365func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
366366 rw.WriteHeader(http.StatusOK)
367− fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
367+ _, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
368+ if err != nil {
369+ logger.Printf("Error writing robots.txt: %s", err.Error())
370+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
371+ }
368372}
369373
370374// ErrorPage writes an error response
@@ -379,13 +383,22 @@ func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, m
379383 Message: message,
380384 ProxyPrefix: p.ProxyPrefix,
381385 }
382− p.templates.ExecuteTemplate(rw, "error.html", t)
386+ err := p.templates.ExecuteTemplate(rw, "error.html", t)
387+ if err != nil {
388+ logger.Printf("Error rendering error.html template: %s", err.Error())
389+ http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
390+ }
383391}
384392
385393// SignInPage writes the sing in template to the response
386394func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
387395 prepareNoCache(rw)
388− p.ClearSessionCookie(rw, req)
396+ err := p.ClearSessionCookie(rw, req)
397+ if err != nil {
398+ logger.Printf("Error clearing session cookie: %s", err.Error())
399+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
400+ return
401+ }
389402 rw.WriteHeader(code)
390403
391404 redirectURL, err := p.GetRedirect(req)
@@ -419,11 +432,15 @@ func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code
419432 if p.providerNameOverride != "" {
420433 t.ProviderName = p.providerNameOverride
421434 }
422− p.templates.ExecuteTemplate(rw, "sign_in.html", t)
435+ err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
436+ if err != nil {
437+ logger.Printf("Error rendering sign_in.html template: %s", err.Error())
438+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
439+ }
423440}
424441
425442// ManualSignIn handles basic auth logins to the proxy
426−func (p *OAuthProxy) ManualSignIn(rw http.ResponseWriter, req *http.Request) (string, bool) {
443+func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
427444 if req.Method != "POST" || p.basicAuthValidator == nil {
428445 return "", false
429446 }
@@ -632,10 +649,15 @@ func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
632649 return
633650 }
634651
635− user, ok := p.ManualSignIn(rw, req)
652+ user, ok := p.ManualSignIn(req)
636653 if ok {
637654 session := &sessionsapi.SessionState{User: user}
638− p.SaveSession(rw, req, session)
655+ err = p.SaveSession(rw, req, session)
656+ if err != nil {
657+ logger.Printf("Error saving session: %s", err.Error())
658+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
659+ return
660+ }
639661 http.Redirect(rw, req, redirect, http.StatusFound)
640662 } else {
641663 if p.SkipProviderButton {
@@ -663,7 +685,11 @@ func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
663685 }
664686 rw.Header().Set("Content-Type", "application/json")
665687 rw.WriteHeader(http.StatusOK)
666− json.NewEncoder(rw).Encode(userInfo)
688+ err = json.NewEncoder(rw).Encode(userInfo)
689+ if err != nil {
690+ logger.Printf("Error encoding user info: %s", err.Error())
691+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
692+ }
667693}
668694
669695// SignOut sends a response to clear the authentication cookie
@@ -674,7 +700,12 @@ func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
674700 p.ErrorPage(rw, 500, "Internal Error", err.Error())
675701 return
676702 }
677− p.ClearSessionCookie(rw, req)
703+ err = p.ClearSessionCookie(rw, req)
704+ if err != nil {
705+ logger.Printf("Error clearing session cookie: %s", err.Error())
706+ p.ErrorPage(rw, 500, "Internal Error", err.Error())
707+ return
708+ }
678709 http.Redirect(rw, req, redirect, http.StatusFound)
679710}
680711
@@ -837,7 +868,10 @@ func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.R
837868 if session != nil && session.Email != "" && !p.Validator(session.Email) {
838869 logger.Printf(session.Email, req, logger.AuthFailure, "Invalid authentication via session: removing session %s", session)
839870 // Invalid session, clear it
840− p.ClearSessionCookie(rw, req)
871+ err := p.ClearSessionCookie(rw, req)
872+ if err != nil {
873+ logger.Printf("Error clearing session cookie: %s", err.Error())
874+ }
841875 return nil, ErrNeedsLogin
842876 }
843877
pkg/authentication/basic/htpasswd.go+16−8
@@ -1,7 +1,7 @@
11package basic
22
33import (
4− "crypto/sha1"
4+ "crypto/sha1" // #nosec G505
55 "encoding/base64"
66 "encoding/csv"
77 "fmt"
@@ -29,11 +29,16 @@ type sha1Pass string
2929// NewHTPasswdValidator constructs an httpasswd based validator from the file
3030// at the path given.
3131func NewHTPasswdValidator(path string) (Validator, error) {
32− r, err := os.Open(path)
32+ r, err := os.Open(path) // #nosec G304
3333 if err != nil {
3434 return nil, fmt.Errorf("could not open htpasswd file: %v", err)
3535 }
36− defer r.Close()
36+ defer func(c io.Closer) {
37+ cerr := c.Close()
38+ if cerr != nil {
39+ logger.Fatalf("error closing the htpasswd file: %v", cerr)
40+ }
41+ }(r)
3742 return newHtpasswd(r)
3843}
3944
@@ -83,13 +88,16 @@ func (h *htpasswdMap) Validate(user string, password string) bool {
8388 return false
8489 }
8590
86− switch real := realPassword.(type) {
91+ switch rp := realPassword.(type) {
8792 case sha1Pass:
88− d := sha1.New()
89− d.Write([]byte(password))
90− return string(real) == base64.StdEncoding.EncodeToString(d.Sum(nil))
93+ d := sha1.New() // #nosec G401
94+ _, err := d.Write([]byte(password))
95+ if err != nil {
96+ return false
97+ }
98+ return string(rp) == base64.StdEncoding.EncodeToString(d.Sum(nil))
9199 case bcryptPass:
92− return bcrypt.CompareHashAndPassword([]byte(real), []byte(password)) == nil
100+ return bcrypt.CompareHashAndPassword([]byte(rp), []byte(password)) == nil
93101 default:
94102 return false
95103 }
pkg/encryption/utils.go+14−2
@@ -2,15 +2,19 @@ package encryption
22
33import (
44 "crypto/hmac"
5− "crypto/sha1"
5+ "crypto/rand"
6+ "crypto/sha1" // #nosec G505
67 "crypto/sha256"
78 "encoding/base64"
89 "fmt"
910 "hash"
11+ "io"
1012 "net/http"
1113 "strconv"
1214 "strings"
1315 "time"
16+
17+ "github.com/oauth2-proxy/oauth2-proxy/pkg/logger"
1418)
1519
1620// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
@@ -76,7 +80,15 @@ func SignedValue(seed string, key string, value []byte, now time.Time) string {
7680func cookieSignature(signer func() hash.Hash, args ...string) string {
7781 h := hmac.New(signer, []byte(args[0]))
7882 for _, arg := range args[1:] {
79− h.Write([]byte(arg))
83+ _, err := h.Write([]byte(arg))
84+ // If signing fails, fail closed and return something that won't validate
85+ if err != nil {
86+ garbage := make([]byte, 16)
87+ if _, err := io.ReadFull(rand.Reader, garbage); err != nil {
88+ logger.Fatal("HMAC & RNG functions both failing. Shutting down for security")
89+ }
90+ return base64.URLEncoding.EncodeToString(garbage)
91+ }
8092 }
8193 var b []byte
8294 b = h.Sum(b)
pkg/logger/logger.go+24−6
@@ -132,13 +132,19 @@ func (l *Logger) Output(calldepth int, message string) {
132132 l.mu.Lock()
133133 defer l.mu.Unlock()
134134
135− l.stdLogTemplate.Execute(l.writer, stdLogMessageData{
135+ err := l.stdLogTemplate.Execute(l.writer, stdLogMessageData{
136136 Timestamp: FormatTimestamp(now),
137137 File: file,
138138 Message: message,
139139 })
140+ if err != nil {
141+ panic(err)
142+ }
140143
141− l.writer.Write([]byte("\n"))
144+ _, err = l.writer.Write([]byte("\n"))
145+ if err != nil {
146+ panic(err)
147+ }
142148}
143149
144150// PrintAuthf writes auth info to the logger. Requires an http.Request to
@@ -160,7 +166,7 @@ func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatu
160166 l.mu.Lock()
161167 defer l.mu.Unlock()
162168
163− l.authTemplate.Execute(l.writer, authLogMessageData{
169+ err := l.authTemplate.Execute(l.writer, authLogMessageData{
164170 Client: client,
165171 Host: req.Host,
166172 Protocol: req.Proto,
@@ -171,8 +177,14 @@ func (l *Logger) PrintAuthf(username string, req *http.Request, status AuthStatu
171177 Status: string(status),
172178 Message: fmt.Sprintf(format, a...),
173179 })
180+ if err != nil {
181+ panic(err)
182+ }
174183
175− l.writer.Write([]byte("\n"))
184+ _, err = l.writer.Write([]byte("\n"))
185+ if err != nil {
186+ panic(err)
187+ }
176188}
177189
178190// PrintReq writes request details to the Logger using the http.Request,
@@ -208,7 +220,7 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
208220 l.mu.Lock()
209221 defer l.mu.Unlock()
210222
211− l.reqTemplate.Execute(l.writer, reqLogMessageData{
223+ err := l.reqTemplate.Execute(l.writer, reqLogMessageData{
212224 Client: client,
213225 Host: req.Host,
214226 Protocol: req.Proto,
@@ -222,8 +234,14 @@ func (l *Logger) PrintReq(username, upstream string, req *http.Request, url url.
222234 UserAgent: fmt.Sprintf("%q", req.UserAgent()),
223235 Username: username,
224236 })
237+ if err != nil {
238+ panic(err)
239+ }
225240
226− l.writer.Write([]byte("\n"))
241+ _, err = l.writer.Write([]byte("\n"))
242+ if err != nil {
243+ panic(err)
244+ }
227245}
228246
229247// GetFileLineString will find the caller file and line number
pkg/middleware/stored_session.go+5−2
@@ -55,7 +55,7 @@ type storedSessionLoader struct {
5555}
5656
5757// loadSession attempts to load a session as identified by the request cookies.
58−// If no session is found, the request will be passed to the nex handler.
58+// If no session is found, the request will be passed to the next handler.
5959// If a session was loader by a previous handler, it will not be replaced.
6060func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
6161 return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
@@ -73,7 +73,10 @@ func (s *storedSessionLoader) loadSession(next http.Handler) http.Handler {
7373 // In the case when there was an error loading the session,
7474 // we should clear the session
7575 logger.Printf("Error loading cookied session: %v, removing session", err)
76− s.store.Clear(rw, req)
76+ err = s.store.Clear(rw, req)
77+ if err != nil {
78+ logger.Printf("Error removing session: %v", err)
79+ }
7780 }
7881
7982 // Add the session to the scope if it was found
pkg/upstream/http.go+2
@@ -103,6 +103,7 @@ func newReverseProxy(target *url.URL, upstream options.Upstream, errorHandler Pr
103103 proxy.FlushInterval = 1 * time.Second
104104 }
105105
106+ /* #nosec G402 */
106107 if upstream.InsecureSkipTLSVerify {
107108 proxy.Transport = &http.Transport{
108109 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
@@ -156,6 +157,7 @@ func newWebSocketReverseProxy(u *url.URL, skipTLSVerify bool) http.Handler {
156157 wsURL := &url.URL{Scheme: wsScheme, Host: u.Host}
157158
158159 wsProxy := wsutil.NewSingleHostReverseProxy(wsURL)
160+ /* #nosec G402 */
159161 if skipTLSVerify {
160162 wsProxy.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
161163 }
pkg/upstream/proxy.go+4−1
@@ -85,6 +85,9 @@ func NewProxyErrorHandler(errorTemplate *template.Template, proxyPrefix string)
8585 Message: "Error proxying to upstream server",
8686 ProxyPrefix: proxyPrefix,
8787 }
88− errorTemplate.Execute(rw, data)
88+ err := errorTemplate.Execute(rw, data)
89+ if err != nil {
90+ http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
91+ }
8992 }
9093}
pkg/util/util.go+1−1
@@ -12,7 +12,7 @@ func GetCertPool(paths []string) (*x509.CertPool, error) {
1212 }
1313 pool := x509.NewCertPool()
1414 for _, path := range paths {
15− data, err := ioutil.ReadFile(path)
15+ data, err := ioutil.ReadFile(path) // #nosec G304
1616 if err != nil {
1717 return nil, fmt.Errorf("certificate authority file (%s) could not be read - %s", path, err)
1818 }
pkg/validation/logging.go+5−2
@@ -13,13 +13,16 @@ func configureLogger(o options.Logging, msgs []string) []string {
1313 // Setup the log file
1414 if len(o.File.Filename) > 0 {
1515 // Validate that the file/dir can be written
16− file, err := os.OpenFile(o.File.Filename, os.O_WRONLY|os.O_CREATE, 0666)
16+ file, err := os.OpenFile(o.File.Filename, os.O_WRONLY|os.O_CREATE, 0600)
1717 if err != nil {
1818 if os.IsPermission(err) {
1919 return append(msgs, "unable to write to log file: "+o.File.Filename)
2020 }
2121 }
22− file.Close()
22+ err = file.Close()
23+ if err != nil {
24+ return append(msgs, "error closing the log file: "+o.File.Filename)
25+ }
2326
2427 logger.Printf("Redirecting logging to file: %s", o.File.Filename)
2528
pkg/validation/options.go+5−1
@@ -30,6 +30,7 @@ func Validate(o *options.Options) error {
3030 msgs = append(msgs, validateSessionCookieMinimal(o)...)
3131
3232 if o.SSLInsecureSkipVerify {
33+ /* #nosec G402 */
3334 insecureTransport := &http.Transport{
3435 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
3536 }
@@ -217,7 +218,10 @@ func Validate(o *options.Options) error {
217218 }
218219
219220 if len(o.TrustedIPs) > 0 && o.ReverseProxy {
220− fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
221+ _, err := fmt.Fprintln(os.Stderr, "WARNING: trusting of IPs with --reverse-proxy poses risks if a header spoofing attack is possible.")
222+ if err != nil {
223+ panic(err)
224+ }
221225 }
222226
223227 for i, ipStr := range o.TrustedIPs {
providers/logingov.go+3−1
@@ -115,10 +115,12 @@ func checkNonce(idToken string, p *LoginGovProvider) (err error) {
115115 return nil, myerr
116116 }
117117 body, myerr := ioutil.ReadAll(resp.Body)
118− resp.Body.Close()
119118 if myerr != nil {
120119 return nil, myerr
121120 }
121+ if myerr = resp.Body.Close(); myerr != nil {
122+ return nil, myerr
123+ }
122124
123125 var pubkeys jose.JSONWebKeySet
124126 myerr = json.Unmarshal(body, &pubkeys)
validator.go+7−1
@@ -3,6 +3,7 @@ package main
33import (
44 "encoding/csv"
55 "fmt"
6+ "io"
67 "os"
78 "strings"
89 "sync/atomic"
@@ -47,7 +48,12 @@ func (um *UserMap) LoadAuthenticatedEmailsFile() {
4748 if err != nil {
4849 logger.Fatalf("failed opening authenticated-emails-file=%q, %s", um.usersFile, err)
4950 }
50− defer r.Close()
51+ defer func(c io.Closer) {
52+ cerr := c.Close()
53+ if cerr != nil {
54+ logger.Fatalf("Error closing authenticated emails file: %s", cerr)
55+ }
56+ }(r)
5157 csvReader := csv.NewReader(r)
5258 csvReader.Comma = ','
5359 csvReader.Comment = '#'
watcher.go+10−2
@@ -41,7 +41,12 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) {
4141 logger.Fatal("failed to create watcher for ", filename, ": ", err)
4242 }
4343 go func() {
44− defer watcher.Close()
44+ defer func(w *fsnotify.Watcher) {
45+ cerr := w.Close()
46+ if cerr != nil {
47+ logger.Fatalf("error closing watcher: %s", err)
48+ }
49+ }(watcher)
4550 for {
4651 select {
4752 case <-done:
@@ -55,7 +60,10 @@ func WatchForUpdates(filename string, done <-chan bool, action func()) {
5560 // can't be opened.
5661 if event.Op&(fsnotify.Remove|fsnotify.Rename|fsnotify.Chmod) != 0 {
5762 logger.Printf("watching interrupted on event: %s", event)
58− watcher.Remove(filename)
63+ err = watcher.Remove(filename)
64+ if err != nil {
65+ logger.Printf("error removing watcher on %s: %s", filename, err)
66+ }
5967 WaitForReplacement(filename, event.Op, watcher)
6068 }
6169 logger.Printf("reloading after event: %s", event)