Skip to content
Merged
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
35 changes: 22 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ The format is based on [*Keep a Changelog*](https://keepachangelog.com/en/1.0.0/
<!--
## [v0.108.0] – TBA

## [v0.107.76] - 2026-06-01 (APPROX.)
## [v0.107.77] - 2026-06-01 (APPROX.)

See also the [v0.107.76 GitHub milestone][ms-v0.107.76].
See also the [v0.107.77 GitHub milestone][ms-v0.107.77].

[ms-v0.107.76]: https://github.com/AdguardTeam/AdGuardHome/milestone/111?closed=1
[ms-v0.107.77]: https://github.com/AdguardTeam/AdGuardHome/milestone/112?closed=1

NOTE: Add new changes BELOW THIS COMMENT.
-->
Expand All @@ -22,32 +22,40 @@ NOTE: Add new changes BELOW THIS COMMENT.

- New `reason` query parameter in `GET /control/querylog`. See `openapi/openapi.yaml` for the full description.

### Deprecated

- Query parameter `response_status` in `GET /control/querylog` is now deprecated. Use new `reason` query parameter instead.

<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->

## [v0.107.76] - 2026-05-21

See also the [v0.107.76 GitHub milestone][ms-v0.107.76].

### Changed

- Duration values in YAML configuration file now support `d` (days) units and has been updated.

**NOTE:** Any rollback to version below the `v0.107.76` should convert the values back to hours.

### Deprecated

- Query parameter `response_status` in `GET /control/querylog` is now deprecated. Use new `reason` query parameter instead.

### Fixed

- DNS caching with disabled DNSSEC ([#8384]).

[#8384]: https://github.com/AdguardTeam/AdGuardHome/issues/8384

<!--
NOTE: Add new changes ABOVE THIS COMMENT.
-->
[ms-v0.107.76]: https://github.com/AdguardTeam/AdGuardHome/milestone/111?closed=1

## [v0.107.75] - 2026-05-19

See also the [v0.107.75 GitHub milestone][ms-v0.107.75].

### Security

- Authorization in GLiNET mode is no longer vulnerable to path traversal attacks.

- Go version has been updated to prevent the possibility of exploiting the Go vulnerabilities fixed in [1.26.3][go-1.26.3].

- IDs of requests received over DoH and DoQ and forwarded to plain-DNS upstreams are now set to non-zero values to improve security.
Expand Down Expand Up @@ -3618,11 +3626,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
[ms-v0.104.2]: https://github.com/AdguardTeam/AdGuardHome/milestone/28?closed=1

<!--
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.76...HEAD
[v0.107.76]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.75...v0.107.76
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.77...HEAD
[v0.107.77]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.76...v0.107.77
-->

[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.75...HEAD
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.76...HEAD
[v0.107.76]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.75...v0.107.76
[v0.107.75]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.74...v0.107.75
[v0.107.74]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.73...v0.107.74
[v0.107.73]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.72...v0.107.73
Expand Down
42 changes: 26 additions & 16 deletions internal/home/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"time"

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

// gliNetTokenRoot is the root where GLiNet tokens are stored. It must not
// be nil if isGLiNet is true.
gliNetTokenRoot *os.Root

// rateLimiter manages the rate limiting for login attempts. It must not be
// nil.
rateLimiter loginRateLimiter
Expand Down Expand Up @@ -88,6 +93,10 @@ type auth struct {
// mux is the server's multiplexer.
mux *http.ServeMux

// gliNetTokenRoot is the root where GLiNet tokens are stored. It must not
// be nil if isGLiNet is true.
gliNetTokenRoot *os.Root

// rateLimiter manages rate limiting for login attempts.
rateLimiter loginRateLimiter

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

return &auth{
logger: conf.baseLogger.With(slogutil.KeyPrefix, "auth"),
mux: conf.mux,
rateLimiter: conf.rateLimiter,
trustedProxies: conf.trustedProxies,
sessions: s,
users: userDB,
doHRoutes: conf.doHRoutes,
isGLiNet: conf.isGLiNet,
isUserless: len(conf.users) == 0,
logger: conf.baseLogger.With(slogutil.KeyPrefix, "auth"),
mux: conf.mux,
rateLimiter: conf.rateLimiter,
trustedProxies: conf.trustedProxies,
gliNetTokenRoot: conf.gliNetTokenRoot,
sessions: s,
users: userDB,
doHRoutes: conf.doHRoutes,
isGLiNet: conf.isGLiNet,
isUserless: len(conf.users) == 0,
}, nil
}

// middleware returns authentication middleware.
func (a *auth) middleware() (mw httputil.Middleware) {
if a.isGLiNet {
return newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{
logger: a.logger,
mux: a.mux,
clock: timeutil.SystemClock{},
doHRoutes: a.doHRoutes,
tokenFilePrefix: glFilePrefix,
ttl: glTokenTimeout,
maxTokenSize: MaxFileSize,
logger: a.logger,
mux: a.mux,
clock: timeutil.SystemClock{},
doHRoutes: a.doHRoutes,
tokenFileRoot: a.gliNetTokenRoot,
ttl: glTokenTimeout,
maxTokenSize: MaxFileSize,
})
}

Expand Down
47 changes: 22 additions & 25 deletions internal/home/authglinet.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ import (
"github.com/AdguardTeam/golibs/timeutil"
)

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

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

// tokenFilePrefix is the prefix of the filepath where the authentication
// token is stored. It must not be empty.
tokenFilePrefix string
// tokenFileRoot is the root where GLiNet tokens are stored. It must not be
// nil.
tokenFileRoot *os.Root

// ttl is the TTL (Time To Live) of the authentication token. It must be
// greater than zero.
Expand All @@ -70,26 +68,26 @@ type authMiddlewareGLiNetConfig struct {
// authMiddlewareGLiNet is the GLiNet authentication middleware. It checks if
// the request is authenticated using a cookie.
type authMiddlewareGLiNet struct {
logger *slog.Logger
mux *http.ServeMux
clock timeutil.Clock
doHRoutes []string
tokenFilePrefix string
ttl time.Duration
maxTokenSize uint
logger *slog.Logger
mux *http.ServeMux
clock timeutil.Clock
doHRoutes []string
tokenFileRoot *os.Root
ttl time.Duration
maxTokenSize uint
}

// newAuthMiddlewareGLiNet returns the new properly initialized
// *authMiddlewareGLiNet.
func newAuthMiddlewareGLiNet(c *authMiddlewareGLiNetConfig) (mw *authMiddlewareGLiNet) {
return &authMiddlewareGLiNet{
logger: c.logger,
mux: c.mux,
clock: c.clock,
doHRoutes: c.doHRoutes,
tokenFilePrefix: c.tokenFilePrefix,
ttl: c.ttl,
maxTokenSize: c.maxTokenSize,
logger: c.logger,
mux: c.mux,
clock: c.clock,
doHRoutes: c.doHRoutes,
tokenFileRoot: c.tokenFileRoot,
ttl: c.ttl,
maxTokenSize: c.maxTokenSize,
}
}

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

Expand Down
48 changes: 39 additions & 9 deletions internal/home/authglinet_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ package home

import (
"encoding/binary"
"io/fs"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"

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

glTokenFileSuffix = "test"

testPerm fs.FileMode = 0o644
)

tempDir := t.TempDir()
glFilePrefix = tempDir + "/gl_token_"
glTokenFile := glFilePrefix + glTokenFileSuffix
glTokenFolder := filepath.Join(tempDir, "foo")
err := os.MkdirAll(glTokenFolder, 0o755)
require.NoError(t, err)

tokenFileRoot, err := os.OpenRoot(glTokenFolder)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, tokenFileRoot.Close)

err = os.MkdirAll(filepath.Join(glTokenFolder, glFilePrefix), testPerm)
require.NoError(t, err)

glTokenFile := filepath.Join(glTokenFolder, glFilePrefix+glTokenFileSuffix)

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

err := os.WriteFile(glTokenFile, glFileData, 0o644)
err = os.WriteFile(glTokenFile, glFileData, testPerm)
require.NoError(t, err)

// Mock token file for testing path traversal vulnerability. See AG-54304.
passwdFile := filepath.Join(tempDir, "path_traversal_token")
err = os.WriteFile(passwdFile, glFileData, testPerm)
require.NoError(t, err)

mw := newAuthMiddlewareGLiNet(&authMiddlewareGLiNetConfig{
logger: testLogger,
mux: http.NewServeMux(),
clock: timeutil.SystemClock{},
tokenFilePrefix: glFilePrefix,
maxTokenSize: MaxFileSize,
ttl: testTTL,
logger: testLogger,
mux: http.NewServeMux(),
clock: timeutil.SystemClock{},
tokenFileRoot: tokenFileRoot,
maxTokenSize: MaxFileSize,
ttl: testTTL,
})

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

reqPathTraversalToken := httptest.NewRequest(http.MethodGet, "/", nil)
reqPathTraversalToken.AddCookie(&http.Cookie{
Name: glCookieName,
Value: "/../../path_traversal_token",
})

testCases := []struct {
req *http.Request
name string
Expand All @@ -66,6 +92,10 @@ func TestAuthMiddlewareGLiNet(t *testing.T) {
req: reqInvalidCookie,
name: "invalid_cookie",
wantCode: http.StatusFound,
}, {
req: reqPathTraversalToken,
name: "path_traversal_token",
wantCode: http.StatusFound,
}}

for _, tc := range testCases {
Expand Down
24 changes: 14 additions & 10 deletions internal/home/authhttp_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,10 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
writeGLFile(t, tempDir, testTTL)
sessionsDB := filepath.Join(tempDir, "sessions.db")

gliNetRoot, err := os.OpenRoot(tempDir)
require.NoError(t, err)
testutil.CleanupAndRequireSuccess(t, gliNetRoot.Close)

mw := &webMw{}
baseMux := http.NewServeMux()
httpReg := aghhttp.NewDefaultRegistrar(baseMux, mw.wrap)
Expand All @@ -529,14 +533,15 @@ func TestAuth_ServeHTTP_auth(t *testing.T) {
require.NoError(t, err)

auth, err := newAuth(testutil.ContextWithTimeout(t, testTimeout), &authConfig{
baseLogger: testLogger,
mux: baseMux,
rateLimiter: emptyRateLimiter{},
trustedProxies: testTrustedProxies,
dbFilename: sessionsDB,
users: users,
sessionTTL: testTTL * time.Second,
isGLiNet: false,
baseLogger: testLogger,
mux: baseMux,
rateLimiter: emptyRateLimiter{},
trustedProxies: testTrustedProxies,
gliNetTokenRoot: gliNetRoot,
dbFilename: sessionsDB,
users: users,
sessionTTL: testTTL * time.Second,
isGLiNet: false,
})
require.NoError(t, err)

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

glFilePrefix = tempDir + "/gl_token_"
glTokenFile := glFilePrefix + "test"
glTokenFile := filepath.Join(tempDir, glFilePrefix+"test")

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