diff --git a/.travis.yml b/.travis.yml index 8c830da..605ae9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: go go: - - 1.7.5 - - 1.8.1 + - 1.10.x + - 1.11.x + - 1.12.x script: - curl -s https://raw.githubusercontent.com/pote/gpm/v1.4.0/bin/gpm > gpm - chmod +x gpm diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e87998..8c024bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +0.5.0 (2021-02-08) +================== +* Fix support for ldap_scope_name + +0.4.0 (2018-11-23) +================== +* URGENT SECURITY FIX: authentication bypass via LDAP passwordless auth LDAP permits passwordless Bind operations by clients - this application verified authentication without checking specifically for an empty password, thus allowing authentication as any valid user by leaving the password field blank. This issue has been present since the first release of this application. + + See also: + * https://github.com/go-ldap/ldap/pull/126 + * https://github.com/pinepain/ldap-auth-proxy/issues/8 + * https://github.com/go-ldap/ldap/issues/93 + +* Added HTTP security headers and prevent caching of proxy pages + +0.3.4 (2018-10-29) +================== +* Make LDAP group comparisons case-insensitive + 0.3.3 (2018-06-21) ================== * Refactor LDAP connection code and use connections more efficiently diff --git a/README.md b/README.md index e54ade7..954a535 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ A reverse proxy and static file server that provides authentication using LDAP. Strongly inspired by [bitly/oauth2_proxy](https://github.com/bitly/oauth2_proxy). -[![Build Status](https://secure.travis-ci.org/ant1441/ldap_proxy.png?branch=master)](http://travis-ci.org/ant1441/ldap_proxy) +[![Build Status](https://travis-ci.com/skybet/ldap_proxy.svg?branch=master)](https://travis-ci.com/skybet/ldap_proxy) ![Screenshot](docs/screenshot.png) ## Installation -1. Download [Prebuilt Binary](https://github.com/skybet/ldap_proxy/releases) (current release is `v2.2`) or build with `$ go get github.com/skybet/ldap_proxy` which will put the binary in `$GOROOT/bin` +1. Download [Prebuilt Binary](https://github.com/skybet/ldap_proxy/releases) or build with `$ go get github.com/skybet/ldap_proxy` which will put the binary in `$GOROOT/bin` 3. Configure Ldap Proxy using config file, command line options, or environment variables 4. Configure SSL or Deploy behind a SSL endpoint (example provided for Nginx) @@ -149,7 +149,7 @@ would be `https://internal.yourcompany.com/`. An example Nginx config follows. Note the use of `Strict-Transport-Security` header to pin requests to SSL via [HSTS](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security): -``` +```nginx server { listen 443 default ssl; server_name internal.yourcompany.com; diff --git a/docs/screenshot.png b/docs/screenshot.png index b06a68f..0d77b11 100644 Binary files a/docs/screenshot.png and b/docs/screenshot.png differ diff --git a/http.go b/http.go index d8049f0..d759234 100644 --- a/http.go +++ b/http.go @@ -48,7 +48,7 @@ func (s *Server) ServeHTTP() { } log.Printf("HTTP: listening on %s", listenAddr) - server := &http.Server{Handler: s.Handler} + server := &http.Server{Handler: XFrameOptionsMiddleware(s.Handler)} err = server.Serve(listener) if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { log.Printf("ERROR: http.Serve() - %s", err) @@ -83,7 +83,7 @@ func (s *Server) ServeHTTPS() { log.Printf("HTTPS: listening on %s", ln.Addr()) tlsListener := tls.NewListener(tcpKeepAliveListener{ln.(*net.TCPListener)}, config) - srv := &http.Server{Handler: s.Handler} + srv := &http.Server{Handler: HSTSMiddleware(XFrameOptionsMiddleware(s.Handler))} err = srv.Serve(tlsListener) if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { @@ -110,3 +110,29 @@ func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil } + +// HSTSMiddleware sets Strict-Transport-Security header +func HSTSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains") + next.ServeHTTP(w, r) + }) +} + +// XFrameOptionsMiddleware sets X-Frame-Options header +func XFrameOptionsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-Frame-Options", "deny") + next.ServeHTTP(w, r) + }) +} + +// NoCache sets no cache headers +func NoCache(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Cache-control", "no-store") + w.Header().Add("Pragma", "no-cache") + + next(w, r) + } +} diff --git a/ldap_connection.go b/ldap_connection.go index eff630c..6c09672 100644 --- a/ldap_connection.go +++ b/ldap_connection.go @@ -66,6 +66,9 @@ func (c *LDAPClient) Close() { // Authenticate authenticates the user against the ldap backend. func (c *LDAPClient) Authenticate(username, password string) (bool, map[string]string, error) { + if username == "" || password == "" { + return false, nil, errors.New("invalid user or password") + } // First bind with a read only user if c.cfg.BindDN != "" && c.cfg.BindPassword != "" { @@ -116,7 +119,7 @@ func (c *LDAPClient) Authenticate(username, password string) (bool, map[string]s if c.cfg.BindDN != "" && c.cfg.BindPassword != "" { err = c.conn.Bind(c.cfg.BindDN, c.cfg.BindPassword) if err != nil { - return true, user, err + return false, user, err } } diff --git a/ldap_proxy.go b/ldap_proxy.go index 3b49ca1..4754a5d 100644 --- a/ldap_proxy.go +++ b/ldap_proxy.go @@ -52,6 +52,7 @@ type LdapProxy struct { AuthOnlyPath string ProxyPrefix string + LdapScopeName string SignInMessage string HtpasswdFile *HtpasswdFile serveMux http.Handler @@ -175,6 +176,7 @@ func NewLdapProxy(opts *Options, validator func(string) bool) *LdapProxy { LdapConfiguration: ldapCfg, LdapGroups: opts.LdapGroups, + LdapScopeName: opts.LdapScopeName, skipAuthRegex: opts.SkipAuthRegex, skipAuthIPs: opts.skipIPs, @@ -220,12 +222,12 @@ func (p *LdapProxy) makeCookie(req *http.Request, name string, value string, exp } } -func (p *LdapProxy) RobotsTxt(rw http.ResponseWriter) { +func (p *LdapProxy) RobotsTxt(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "User-agent: *\nDisallow: /") } -func (p *LdapProxy) PingPage(rw http.ResponseWriter) { +func (p *LdapProxy) PingPage(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(http.StatusOK) fmt.Fprintf(rw, "OK") } @@ -267,6 +269,7 @@ func (p *LdapProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code i ProxyPrefix string Footer template.HTML }{ + LdapScopeName: p.LdapScopeName, SignInMessage: p.SignInMessage, Failed: failed, Redirect: redirectURL, @@ -396,17 +399,17 @@ func (p *LdapProxy) getRemoteAddrStr(req *http.Request) (s string) { func (p *LdapProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { switch path := req.URL.Path; { case path == p.RobotsPath: - p.RobotsTxt(rw) + NoCache(p.RobotsTxt)(rw, req) case path == p.PingPath: - p.PingPage(rw) + NoCache(p.PingPage)(rw, req) case p.IsWhitelistedRequest(req): p.serveMux.ServeHTTP(rw, req) case path == p.SignInPath: - p.SignIn(rw, req) + NoCache(p.SignIn)(rw, req) case path == p.SignOutPath: - p.SignOut(rw, req) + NoCache(p.SignOut)(rw, req) case path == p.AuthOnlyPath: - p.AuthenticateOnly(rw, req) + NoCache(p.AuthenticateOnly)(rw, req) default: p.Proxy(rw, req) } @@ -464,7 +467,7 @@ func (p *LdapProxy) SignIn(rw http.ResponseWriter, req *http.Request) { } if len(p.LdapGroups) > 0 { - if sliceContains(p.LdapGroups, groups) { + if sliceContainsString(p.LdapGroups, groups) { if err := p.SaveSession(rw, req, session); err != nil { log.Printf("failed to save session %v", err) } @@ -637,11 +640,11 @@ func (p *LdapProxy) CheckBasicAuth(req *http.Request) (*SessionState, error) { return nil, fmt.Errorf("%s not in HtpasswdFile", pair[0]) } -// sliceContains returns true if a and b contains any common string -func sliceContains(a, b []string) bool { +// sliceContainsString returns true if a and b contains any common string ignoring case +func sliceContainsString(a, b []string) bool { for _, aItem := range a { for _, bItem := range b { - if aItem == bItem { + if strings.ToLower(aItem) == strings.ToLower(bItem) { return true } } diff --git a/ldap_proxy_test.go b/ldap_proxy_test.go index a3b7e0f..feac4d7 100644 --- a/ldap_proxy_test.go +++ b/ldap_proxy_test.go @@ -120,7 +120,7 @@ func (fnc *fakeNetConn) Read(p []byte) (n int, err error) { return 0, io.EOF } -func TestSliceContains(t *testing.T) { +func TestSliceContainsString(t *testing.T) { testCases := []struct { desc string a []string @@ -133,6 +133,12 @@ func TestSliceContains(t *testing.T) { b: []string{"b"}, expect: true, }, + { + desc: "happy path case insensitive", + a: []string{"a", "B", "c"}, + b: []string{"b"}, + expect: true, + }, { desc: "empty", a: []string{}, @@ -149,7 +155,7 @@ func TestSliceContains(t *testing.T) { for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { - if res := sliceContains(tC.a, tC.b); res != tC.expect { + if res := sliceContainsString(tC.a, tC.b); res != tC.expect { t.Errorf("with a %+v and b %+v, expected %+v, got %v", tC.a, tC.b, tC.expect, res) } }) diff --git a/templates.go b/templates.go index 5147dcd..8efa67c 100644 --- a/templates.go +++ b/templates.go @@ -118,7 +118,7 @@ func getTemplates() *template.Template { {{ if .SignInMessage }}

{{.SignInMessage}}

{{ end}} -

Sign in with a {{.LdapScopeName}} Account

+

Sign in with your {{.LdapScopeName}} account

{{ if .Failed }} diff --git a/version.go b/version.go index 9b05097..0a58c40 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package main // VERSION released -const VERSION = "0.3.3" +const VERSION = "0.5.0"