Skip to content

Commit 249a3a1

Browse files
committed
Pull request 2656: AG-54304-path-traversal-vuln
Squashed commit of the following: commit 33991de Merge: 3f88261 83d8d65 Author: f.setrakov <f.setrakov@adguard.com> Date: Fri May 22 16:10:53 2026 +0300 Merge branch 'master' into AG-54304-path-traversal-vuln commit 3f88261 Author: f.setrakov <f.setrakov@adguard.com> Date: Thu May 21 17:13:37 2026 +0300 home: imp code commit 589589f Author: f.setrakov <f.setrakov@adguard.com> Date: Wed May 20 15:42:34 2026 +0300 all: fix race, imp code commit 762f073 Author: f.setrakov <f.setrakov@adguard.com> Date: Tue May 19 18:13:24 2026 +0300 all: fix path-traversal vuln
1 parent 83d8d65 commit 249a3a1

9 files changed

Lines changed: 175 additions & 110 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ See also the [v0.107.75 GitHub milestone][ms-v0.107.75].
5454

5555
### Security
5656

57+
- Authorization in GLiNET mode is no longer vulnerable to path traversal attacks.
58+
5759
- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.26.3][go-1.26.3].
5860

5961
- IDs of requests received over DoH and DoQ and forwarded to plain-DNS upstreams are now set to non-zero values to improve security.

internal/home/auth.go

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"log/slog"
77
"net/http"
8+
"os"
89
"time"
910

1011
"github.com/AdguardTeam/AdGuardHome/internal/aghuser"
@@ -56,6 +57,10 @@ type authConfig struct {
5657
// mux is the server's multiplexer. It must not be nil.
5758
mux *http.ServeMux
5859

60+
// gliNetTokenRoot is the root where GLiNet tokens are stored. It must not
61+
// be nil if isGLiNet is true.
62+
gliNetTokenRoot *os.Root
63+
5964
// rateLimiter manages the rate limiting for login attempts. It must not be
6065
// nil.
6166
rateLimiter loginRateLimiter
@@ -88,6 +93,10 @@ type auth struct {
8893
// mux is the server's multiplexer.
8994
mux *http.ServeMux
9095

96+
// gliNetTokenRoot is the root where GLiNet tokens are stored. It must not
97+
// be nil if isGLiNet is true.
98+
gliNetTokenRoot *os.Root
99+
91100
// rateLimiter manages rate limiting for login attempts.
92101
rateLimiter loginRateLimiter
93102

@@ -133,29 +142,30 @@ func newAuth(ctx context.Context, conf *authConfig) (a *auth, err error) {
133142
}
134143

135144
return &auth{
136-
logger: conf.baseLogger.With(slogutil.KeyPrefix, "auth"),
137-
mux: conf.mux,
138-
rateLimiter: conf.rateLimiter,
139-
trustedProxies: conf.trustedProxies,
140-
sessions: s,
141-
users: userDB,
142-
doHRoutes: conf.doHRoutes,
143-
isGLiNet: conf.isGLiNet,
144-
isUserless: len(conf.users) == 0,
145+
logger: conf.baseLogger.With(slogutil.KeyPrefix, "auth"),
146+
mux: conf.mux,
147+
rateLimiter: conf.rateLimiter,
148+
trustedProxies: conf.trustedProxies,
149+
gliNetTokenRoot: conf.gliNetTokenRoot,
150+
sessions: s,
151+
users: userDB,
152+
doHRoutes: conf.doHRoutes,
153+
isGLiNet: conf.isGLiNet,
154+
isUserless: len(conf.users) == 0,
145155
}, nil
146156
}
147157

148158
// middleware returns authentication middleware.
149159
func (a *auth) middleware() (mw httputil.Middleware) {
150160
if a.isGLiNet {
151161
return newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{
152-
logger: a.logger,
153-
mux: a.mux,
154-
clock: timeutil.SystemClock{},
155-
doHRoutes: a.doHRoutes,
156-
tokenFilePrefix: glFilePrefix,
157-
ttl: glTokenTimeout,
158-
maxTokenSize: MaxFileSize,
162+
logger: a.logger,
163+
mux: a.mux,
164+
clock: timeutil.SystemClock{},
165+
doHRoutes: a.doHRoutes,
166+
tokenFileRoot: a.gliNetTokenRoot,
167+
ttl: glTokenTimeout,
168+
maxTokenSize: MaxFileSize,
159169
})
160170
}
161171

internal/home/authglinet.go

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ import (
1818
"github.com/AdguardTeam/golibs/timeutil"
1919
)
2020

21-
// glFilePrefix is the prefix of the filepath where the authentication token is
22-
// stored. Note that it is variable so it can be edited in tests.
23-
//
24-
// TODO(s.chzhen): Make it a constant.
25-
var glFilePrefix = "/tmp/gl_token_"
21+
// glFilePrefix is the prefix of the file where the authentication token is
22+
// stored.
23+
const glFilePrefix = "gl_token_"
2624

2725
const (
2826
// glTokenTimeout is the TTL (Time To Live) of the authentication token.
@@ -54,9 +52,9 @@ type authMiddlewareGLiNetConfig struct {
5452
// doHRoutes is a list of DoH routes for public access.
5553
doHRoutes []string
5654

57-
// tokenFilePrefix is the prefix of the filepath where the authentication
58-
// token is stored. It must not be empty.
59-
tokenFilePrefix string
55+
// tokenFileRoot is the root where GLiNet tokens are stored. It must not be
56+
// nil.
57+
tokenFileRoot *os.Root
6058

6159
// ttl is the TTL (Time To Live) of the authentication token. It must be
6260
// greater than zero.
@@ -70,26 +68,26 @@ type authMiddlewareGLiNetConfig struct {
7068
// authMiddlewareGLiNet is the GLiNet authentication middleware. It checks if
7169
// the request is authenticated using a cookie.
7270
type authMiddlewareGLiNet struct {
73-
logger *slog.Logger
74-
mux *http.ServeMux
75-
clock timeutil.Clock
76-
doHRoutes []string
77-
tokenFilePrefix string
78-
ttl time.Duration
79-
maxTokenSize uint
71+
logger *slog.Logger
72+
mux *http.ServeMux
73+
clock timeutil.Clock
74+
doHRoutes []string
75+
tokenFileRoot *os.Root
76+
ttl time.Duration
77+
maxTokenSize uint
8078
}
8179

8280
// newAuthMiddlewareGLiNet returns the new properly initialized
8381
// *authMiddlewareGLiNet.
8482
func newAuthMiddlewareGLiNet(c *authMiddlewareGLiNetConfig) (mw *authMiddlewareGLiNet) {
8583
return &authMiddlewareGLiNet{
86-
logger: c.logger,
87-
mux: c.mux,
88-
clock: c.clock,
89-
doHRoutes: c.doHRoutes,
90-
tokenFilePrefix: c.tokenFilePrefix,
91-
ttl: c.ttl,
92-
maxTokenSize: c.maxTokenSize,
84+
logger: c.logger,
85+
mux: c.mux,
86+
clock: c.clock,
87+
doHRoutes: c.doHRoutes,
88+
tokenFileRoot: c.tokenFileRoot,
89+
ttl: c.ttl,
90+
maxTokenSize: c.maxTokenSize,
9391
}
9492
}
9593

@@ -158,8 +156,7 @@ func (mw *authMiddlewareGLiNet) isAuthenticated(ctx context.Context, r *http.Req
158156
// the time stored in a file named after the token and checks if the token has
159157
// expired based on that time.
160158
func (mw *authMiddlewareGLiNet) checkToken(ctx context.Context, token string) (ok bool) {
161-
tokenFile := mw.tokenFilePrefix + token
162-
tokenDate := mw.tokenDate(ctx, tokenFile)
159+
tokenDate := mw.tokenDate(ctx, glFilePrefix+token)
163160
now := mw.clock.Now()
164161
if now.Before(tokenDate.Add(mw.ttl)) {
165162
return true
@@ -173,7 +170,7 @@ func (mw *authMiddlewareGLiNet) checkToken(ctx context.Context, token string) (o
173170
// tokenDate returns the time stored in the authentication token file. If there
174171
// is an error, it logs the error and returns the zero time.
175172
func (mw *authMiddlewareGLiNet) tokenDate(ctx context.Context, tokenFile string) (t time.Time) {
176-
f, err := os.Open(tokenFile)
173+
f, err := mw.tokenFileRoot.Open(tokenFile)
177174
if err != nil {
178175
mw.logger.ErrorContext(ctx, "opening token file", slogutil.KeyError, err)
179176

internal/home/authglinet_internal_test.go

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package home
22

33
import (
44
"encoding/binary"
5+
"io/fs"
56
"net/http"
67
"net/http/httptest"
78
"os"
9+
"path/filepath"
810
"testing"
911
"time"
1012

13+
"github.com/AdguardTeam/golibs/testutil"
1114
"github.com/AdguardTeam/golibs/timeutil"
1215
"github.com/stretchr/testify/assert"
1316
"github.com/stretchr/testify/require"
@@ -20,25 +23,42 @@ func TestAuthMiddlewareGLiNet(t *testing.T) {
2023
testTTL = 60 * time.Second
2124

2225
glTokenFileSuffix = "test"
26+
27+
testPerm fs.FileMode = 0o644
2328
)
2429

2530
tempDir := t.TempDir()
26-
glFilePrefix = tempDir + "/gl_token_"
27-
glTokenFile := glFilePrefix + glTokenFileSuffix
31+
glTokenFolder := filepath.Join(tempDir, "foo")
32+
err := os.MkdirAll(glTokenFolder, 0o755)
33+
require.NoError(t, err)
34+
35+
tokenFileRoot, err := os.OpenRoot(glTokenFolder)
36+
require.NoError(t, err)
37+
testutil.CleanupAndRequireSuccess(t, tokenFileRoot.Close)
38+
39+
err = os.MkdirAll(filepath.Join(glTokenFolder, glFilePrefix), testPerm)
40+
require.NoError(t, err)
41+
42+
glTokenFile := filepath.Join(glTokenFolder, glFilePrefix+glTokenFileSuffix)
2843

2944
glFileData := make([]byte, 4)
3045
binary.NativeEndian.PutUint32(glFileData, uint32(time.Now().Add(testTTL).Unix()))
3146

32-
err := os.WriteFile(glTokenFile, glFileData, 0o644)
47+
err = os.WriteFile(glTokenFile, glFileData, testPerm)
48+
require.NoError(t, err)
49+
50+
// Mock token file for testing path traversal vulnerability. See AG-54304.
51+
passwdFile := filepath.Join(tempDir, "path_traversal_token")
52+
err = os.WriteFile(passwdFile, glFileData, testPerm)
3353
require.NoError(t, err)
3454

3555
mw := newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{
36-
logger: testLogger,
37-
mux: http.NewServeMux(),
38-
clock: timeutil.SystemClock{},
39-
tokenFilePrefix: glFilePrefix,
40-
maxTokenSize: MaxFileSize,
41-
ttl: testTTL,
56+
logger: testLogger,
57+
mux: http.NewServeMux(),
58+
clock: timeutil.SystemClock{},
59+
tokenFileRoot: tokenFileRoot,
60+
maxTokenSize: MaxFileSize,
61+
ttl: testTTL,
4262
})
4363

4464
h := &testAuthHandler{}
@@ -50,6 +70,12 @@ func TestAuthMiddlewareGLiNet(t *testing.T) {
5070
reqInvalidCookie := httptest.NewRequest(http.MethodGet, "/", nil)
5171
reqInvalidCookie.AddCookie(&http.Cookie{Name: glCookieName, Value: "invalid_cookie"})
5272

73+
reqPathTraversalToken := httptest.NewRequest(http.MethodGet, "/", nil)
74+
reqPathTraversalToken.AddCookie(&http.Cookie{
75+
Name: glCookieName,
76+
Value: "/../../path_traversal_token",
77+
})
78+
5379
testCases := []struct {
5480
req *http.Request
5581
name string
@@ -66,6 +92,10 @@ func TestAuthMiddlewareGLiNet(t *testing.T) {
6692
req: reqInvalidCookie,
6793
name: "invalid_cookie",
6894
wantCode: http.StatusFound,
95+
}, {
96+
req: reqPathTraversalToken,
97+
name: "path_traversal_token",
98+
wantCode: http.StatusFound,
6999
}}
70100

71101
for _, tc := range testCases {

internal/home/authhttp_internal_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,10 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
517517
writeGLFile(t, tempDir, testTTL)
518518
sessionsDB := filepath.Join(tempDir, "sessions.db")
519519

520+
gliNetRoot, err := os.OpenRoot(tempDir)
521+
require.NoError(t, err)
522+
testutil.CleanupAndRequireSuccess(t, gliNetRoot.Close)
523+
520524
mw := &webMw{}
521525
baseMux := http.NewServeMux()
522526
httpReg := aghhttp.NewDefaultRegistrar(baseMux, mw.wrap)
@@ -529,14 +533,15 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
529533
require.NoError(t, err)
530534

531535
auth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{
532-
baseLogger: testLogger,
533-
mux: baseMux,
534-
rateLimiter: emptyRateLimiter{},
535-
trustedProxies: testTrustedProxies,
536-
dbFilename: sessionsDB,
537-
users: users,
538-
sessionTTL: testTTL * time.Second,
539-
isGLiNet: false,
536+
baseLogger: testLogger,
537+
mux: baseMux,
538+
rateLimiter: emptyRateLimiter{},
539+
trustedProxies: testTrustedProxies,
540+
gliNetTokenRoot: gliNetRoot,
541+
dbFilename: sessionsDB,
542+
users: users,
543+
sessionTTL: testTTL * time.Second,
544+
isGLiNet: false,
540545
})
541546
require.NoError(t, err)
542547

@@ -620,8 +625,7 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
620625
func writeGLFile(t *testing.T, tempDir string, testTTL int64) {
621626
t.Helper()
622627

623-
glFilePrefix = tempDir + "/gl_token_"
624-
glTokenFile := glFilePrefix + "test"
628+
glTokenFile := filepath.Join(tempDir, glFilePrefix+"test")
625629

626630
glFileData := make([]byte, 4)
627631
binary.NativeEndian.PutUint32(glFileData, uint32(time.Now().Unix()+testTTL))

0 commit comments

Comments
 (0)