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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ coverage.out
*.json
!map-countries-seed.json
!team-logos-seed.json
!backend/pkg/i18n/locales/*.json
*.yaml
!mapctf.example.yaml
*.yml
Expand Down
15 changes: 15 additions & 0 deletions backend/cmd/api/handlers/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,18 @@ func getRealIP(r *http.Request) string {
}
return ip
}

// RealIP is a middleware that overrides r.RemoteAddr with the originating
// client IP resolved from the X-Real-Ip / X-Forwarded-For headers. It replaces
// chi's deprecated middleware.RealIP, which is vulnerable to IP spoofing
// because it trusts those headers unconditionally regardless of whether the
// upstream infrastructure actually sets them. Use this only behind a trusted
// reverse proxy that populates the headers.
func RealIP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ip := getRealIP(r); ip != "" {
r.RemoteAddr = ip
}
next.ServeHTTP(w, r)
})
}
14 changes: 7 additions & 7 deletions backend/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ const (
apiAdminPath = "/admin"
// API teams path
apiTeamsPath = "/teams"
// API users path
apiUsersPath = "/users"
// API settings path
apiSettingsPath = "/settings"
// API challenges path
Expand Down Expand Up @@ -248,7 +246,7 @@ func mapCTFService() {
}))
// Middleware
muxAPI.Use(middleware.RequestID)
muxAPI.Use(middleware.RealIP)
muxAPI.Use(handlers.RealIP)
muxAPI.Use(middleware.Recoverer)
// Root
muxAPI.Get(rootPath, handlersCTF.RootHandler)
Expand Down Expand Up @@ -281,12 +279,12 @@ func mapCTFService() {
r.Use(handlersCTF.RequireAdmin)
r.Get(apiTeamsPath, handlersCTF.AdminTeamsHandler) // GET /api/v1/admin/{uuid}/teams
r.Post(apiTeamsPath, handlersCTF.CreateTeamHandler) // POST /api/v1/admin/{uuid}/teams
//r.Delete(apiTeamsPath+"/{entID}/{id}", handlersCTF.DeleteTeamHandler) // DELETE /api/v1/admin/teams/{entID}/{id}
// r.Delete(apiTeamsPath+"/{entID}/{id}", handlersCTF.DeleteTeamHandler) // DELETE /api/v1/admin/teams/{entID}/{id}
r.Get(apiSettingsPath, handlersCTF.SettingsHandler) // GET /api/v1/admin/{uuid}/settings
r.Get(apiChallengesPath, handlersCTF.AdminChallengesHandler) // GET /api/v1/admin/{uuid}/challenges
r.Post(apiChallengesPath, handlersCTF.CreateChallengeHandler) // POST /api/v1/admin/{uuid}/challenges
//r.Patch(apiChallengesPath+"/{entID}/{id}", handlersCTF.UpdateChallengeHandler) // PATCH /api/v1/admin/challenges/{entID}/{id}
//r.Delete(apiChallengesPath+"/{entID}/{id}", handlersCTF.DeleteChallengeHandler) // DELETE /api/v1/admin/challenges/{entID}/{id}
// r.Patch(apiChallengesPath+"/{entID}/{id}", handlersCTF.UpdateChallengeHandler) // PATCH /api/v1/admin/challenges/{entID}/{id}
// r.Delete(apiChallengesPath+"/{entID}/{id}", handlersCTF.DeleteChallengeHandler) // DELETE /api/v1/admin/challenges/{entID}/{id}
})
})
})
Expand Down Expand Up @@ -558,7 +556,9 @@ func main() {
if usersMgr.Exists(username, uuid) {
// User exists, reset password
log.Info().Msgf("User '%s' already exists for UUID %s, resetting password...", username, uuid)
usersMgr.SetPassword(username, password, uuid)
if err := usersMgr.SetPassword(username, password, uuid); err != nil {
return fmt.Errorf("failed to reset password for admin user '%s': %w", username, err)
}
fmt.Printf("Password reset successfully for admin user '%s' (UUID %s).\n", username, uuid)
} else {
// User doesn't exist, create it
Expand Down
806 changes: 404 additions & 402 deletions backend/cmd/map/handlers/admin.go

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions backend/cmd/map/handlers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,46 +25,46 @@ func (h *HandlersMap) LoginPOSTHandler(w http.ResponseWriter, r *http.Request) {
// Get UUID from URL path parameters and validate it
uuid := chi.URLParam(r, "uuid")
if uuid == "" || uuid != h.Config.Map.UUID {
log.Err(errors.New("Invalid UUID")).Msgf("UUID: %s", uuid)
log.Err(errors.New("invalid UUID")).Msgf("UUID: %s", uuid)
h.ErrorInvalidUUID(w, r)
return
}
var l MapLoginRequest
// Parse request JSON body
if err := json.NewDecoder(r.Body).Decode(&l); err != nil {
log.Err(err).Msg("error parsing POST body")
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: "error parsing POST body"})
log.Err(err).Msg(h.T(r.Context())("auth.error_parse_body"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: h.T(r.Context())("auth.error_parse_body")})
return
}
if l.Username == "" || l.Password == "" {
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: "username and password are required"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: h.T(r.Context())("auth.user_pass_required")})
return
}
valid, user := h.Users.CheckLoginCredentials(l.Username, l.Password, uuid)
if !valid {
HTTPResponse(w, JSONApplicationUTF8, http.StatusUnauthorized, MapErrorResponse{Error: "invalid credentials"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusUnauthorized, MapErrorResponse{Error: h.T(r.Context())("auth.invalid_credentials")})
return
}
if !user.Admin {
if h.Settings == nil {
log.Err(errors.New("settings manager not initialized")).Msg("error checking login_enabled")
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: "login is unavailable"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("auth.login_unavailable")})
return
}
loginEnabled, err := h.Settings.GetLoginEnabled(uuid)
if err != nil {
log.Err(err).Msg("error getting login enabled setting")
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: "login is unavailable"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("auth.login_unavailable")})
return
}
if !loginEnabled {
HTTPResponse(w, JSONApplicationUTF8, http.StatusForbidden, MapErrorResponse{Error: "login is disabled"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusForbidden, MapErrorResponse{Error: h.T(r.Context())("auth.login_disabled")})
return
}
}
err := h.Sessions.RenewToken(r.Context())
if err != nil {
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: "error renewing session"})
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("auth.error_renew_session")})
return
}
h.Sessions.Put(r.Context(), string(ContextKeyUser), user.Username)
Expand All @@ -79,7 +79,7 @@ func (h *HandlersMap) LoginPOSTHandler(w http.ResponseWriter, r *http.Request) {
}
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, MapLoginResponse{
Success: true,
Message: "Login successful",
Message: h.T(r.Context())("login.success_msg"),
Redirect: redirectTo,
})
}
Expand All @@ -92,19 +92,19 @@ func (h *HandlersMap) LogoutPOSTHandler(w http.ResponseWriter, r *http.Request)
// Get UUID from URL path
uuid := chi.URLParam(r, "uuid")
if uuid == "" {
log.Err(errors.New("UUID is required")).Msg("UUID is required")
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: "UUID is required"})
log.Err(errors.New(h.T(r.Context())("auth.uuid_required"))).Msg(h.T(r.Context())("auth.uuid_required"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusBadRequest, MapErrorResponse{Error: h.T(r.Context())("auth.uuid_required")})
return
}
if err := h.Sessions.Destroy(r.Context()); err != nil {
log.Err(err).Msg("error destroying session")
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: "error destroying session"})
log.Err(err).Msg(h.T(r.Context())("auth.error_destroy_session"))
HTTPResponse(w, JSONApplicationUTF8, http.StatusInternalServerError, MapErrorResponse{Error: h.T(r.Context())("auth.error_destroy_session")})
return
}
// Send response
HTTPResponse(w, JSONApplicationUTF8, http.StatusOK, MapLogoutResponse{
Success: true,
Message: "Logout successful",
Message: h.T(r.Context())("login.logout_msg"),
Redirect: "/" + uuid + "/login",
})
}
Expand Down
40 changes: 40 additions & 0 deletions backend/cmd/map/handlers/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/alexedwards/scs/v2"
"github.com/jmpsec/mapctf/pkg/config"
"github.com/jmpsec/mapctf/pkg/i18n"
"github.com/jmpsec/mapctf/pkg/settings"
"github.com/jmpsec/mapctf/pkg/users"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -166,3 +167,42 @@ func TestLoginPOSTHandlerAllowsNonAdminWhenLoginEnabled(t *testing.T) {
require.Equal(t, "alice", sessions.GetString(req.Context(), string(ContextKeyUser)))
require.False(t, sessions.GetBool(req.Context(), string(ContextKeyAdmin)))
}

func TestLoginPOSTHandlerReturnsLocalizedMessage(t *testing.T) {
db := newJSONTestDB(t)
userManager, err := users.CreateUserManager(db, &config.ConfigurationJWT{Secret: "test-secret", HoursToExpire: 24})
require.NoError(t, err)
settingsManager, err := settings.CreateSettingsManager(db, "test-service")
require.NoError(t, err)
require.NoError(t, settingsManager.Initialization(jsonTestUUID))
require.NoError(t, settingsManager.SetLanguage("es", jsonSettingsAuthor, jsonTestUUID))

catalog, err := i18n.New()
require.NoError(t, err)
sessions := scs.New()
handler := CreateHandlersMap(
WithConfig(config.MapCTFConfiguration{Map: config.ConfigurationMap{UUID: jsonTestUUID}}),
WithUsers(userManager),
WithSettings(settingsManager),
WithSessions(sessions),
WithI18N(catalog),
)

admin, err := userManager.New("admin", "password123", "admin@example.com", "Admin", true, false, jsonTestUUID, 0)
require.NoError(t, err)
require.NoError(t, userManager.Create(admin))

req := newJSONBodyRequestWithUUID(http.MethodPost, "/login", jsonTestUUID, MapLoginRequest{Username: "admin", Password: "password123"})
ctx, err := sessions.Load(req.Context(), "")
require.NoError(t, err)
req = req.WithContext(ctx)

rec := httptest.NewRecorder()
handler.LocaleMiddleware(http.HandlerFunc(handler.LoginPOSTHandler)).ServeHTTP(rec, req)

require.Equal(t, http.StatusOK, rec.Code)
var resp MapLoginResponse
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
require.True(t, resp.Success)
require.Equal(t, "Acceso correcto", resp.Message)
}
4 changes: 2 additions & 2 deletions backend/cmd/map/handlers/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ func (h *HandlersMap) ErrorHandler(w http.ResponseWriter, r *http.Request) {
}
}
templateData := ErrorTemplateData{
Title: "MapCTF: Error",
Error: msg,
Title: "MapCTF: Error",
Error: msg,
Status: status,
Header: header,
}
Expand Down
Loading
Loading