Files
nginx-ui/api/sites/security_test.go
Hintay 82c637c36b fix: expand TLS includes for maintenance mode (#1692)
* fix: expand TLS includes for maintenance mode

Preserve maintenance-mode TLS handshake behavior by expanding allowed include files into ssl directives instead of copying include directives verbatim.

* fix: harden maintenance include path validation

Validate maintenance include paths before file-system access and add regression coverage for relative path escapes.

* refactor(site): simplify maintenance include expansion and tests
2026-05-23 03:19:40 +09:00

110 lines
2.9 KiB
Go

package sites
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/0xJacky/Nginx-UI/internal/cache"
"github.com/0xJacky/Nginx-UI/internal/middleware"
internaluser "github.com/0xJacky/Nginx-UI/internal/user"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
cosysettings "github.com/uozi-tech/cosy/settings"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupSiteSecurityTest(t *testing.T) string {
t.Helper()
gin.SetMode(gin.TestMode)
cache.InitInMemoryCache()
originalJWTSecret := cosysettings.AppSettings.JwtSecret
cosysettings.AppSettings.JwtSecret = "test-secret"
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
if err != nil {
t.Fatalf("failed to open test db: %v", err)
}
if err := db.AutoMigrate(&model.User{}, &model.AuthToken{}, &model.Passkey{}); err != nil {
t.Fatalf("failed to migrate test db: %v", err)
}
model.Use(db)
query.Use(db)
query.SetDefault(db)
otpUser := &model.User{
Model: model.Model{ID: 2},
Name: "otp",
Status: true,
Language: "en",
OTPSecret: []byte("otp-enabled"),
}
if err := db.Create(otpUser).Error; err != nil {
t.Fatalf("failed to create test user: %v", err)
}
payload, err := internaluser.GenerateJWT(otpUser)
if err != nil {
t.Fatalf("failed to create token: %v", err)
}
t.Cleanup(func() {
cache.Shutdown()
cosysettings.AppSettings.JwtSecret = originalJWTSecret
})
return payload.Token
}
func TestSiteSaveRequiresSecureSessionForOTPUser(t *testing.T) {
token := setupSiteSecurityTest(t)
router := gin.New()
group := router.Group("/", middleware.AuthRequired())
mutations := group.Group("", middleware.RequireSecureSession())
mutations.POST("sites/:name", SaveSite)
body, err := json.Marshal(gin.H{
"content": "server {\n listen 80;\n}\n",
})
if err != nil {
t.Fatalf("failed to marshal request body: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/sites/example.com", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", token)
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, req)
if recorder.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", recorder.Code)
}
}
func TestEnableMaintenanceSiteRejectsInvalidSiteName(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Params = gin.Params{{Key: "name", Value: "..%2F..%2Fnginx.conf"}}
EnableMaintenanceSite(c)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d with body %q", recorder.Code, recorder.Body.String())
}
if !bytes.Contains(recorder.Body.Bytes(), []byte("invalid site name")) {
t.Fatalf("expected invalid site name response, got %q", recorder.Body.String())
}
}