From 855c7c415310d4797d496bbc8cabaf6fd2961c66 Mon Sep 17 00:00:00 2001 From: chchen7 Date: Sun, 28 Dec 2025 18:09:41 +0800 Subject: [PATCH 1/6] fix: uecm mandatory IE check --- internal/sbi/api_uecontextmanagement.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index d1f159b..b017410 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -321,6 +321,30 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { c.JSON(int(rsp.Status), rsp) return } + // TS 29.503 6.2.6.2.2 requirements check + missingIE := "" + if amf3GppAccessRegistration.AmfInstanceId == "" { + missingIE = "AmfInstanceId" + } else if amf3GppAccessRegistration.Guami == nil { + missingIE = "Guami" + } else if amf3GppAccessRegistration.DeregCallbackUri == "" { + missingIE = "DeregCallbackUri" + } else if amf3GppAccessRegistration.RatType == "" { + missingIE = "RatType" + } + + if missingIE != "" { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE " + missingIE + " is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } logger.UecmLog.Infof("Handle RegistrationAmf3gppAccess") From 4fe6b46a27eea25f611328085e0fe00e25232628 Mon Sep 17 00:00:00 2001 From: chchen7 Date: Mon, 29 Dec 2025 01:26:06 +0800 Subject: [PATCH 2/6] fix: validate ueID format --- internal/sbi/api_uecontextmanagement.go | 141 ++++++++++++++++++++++-- internal/util/gpsi.go | 33 ++++++ internal/util/gpsi_test.go | 70 ++++++++++++ internal/util/supi.go | 39 +++++++ internal/util/supi_test.go | 76 +++++++++++++ 5 files changed, 351 insertions(+), 8 deletions(-) create mode 100644 internal/util/gpsi.go create mode 100644 internal/util/gpsi_test.go create mode 100644 internal/util/supi.go create mode 100644 internal/util/supi_test.go diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index b017410..8c4c622 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -10,6 +10,7 @@ import ( Nudr_DataRepository "github.com/free5gc/openapi/udr/DataRepository" "github.com/free5gc/udm/internal/logger" "github.com/free5gc/util/metrics/sbi" + "github.com/free5gc/udm/internal/util" ) func (s *Server) getUEContextManagementRoutes() []Route { @@ -245,6 +246,21 @@ func (s *Server) HandleGetAmfNon3gppAccess(c *gin.Context) { logger.UecmLog.Infoln("Handle GetAmfNon3gppAccessRequest") ueId := c.Param("ueId") + // TS 29.503 5.3.2.5.3 + // Validate SUPI and GPSI format the UE ID (SUPI or GPSI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueId) || util.IsValidGpsi(ueId) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueId) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } supportedFeatures := c.Query("supported-features") var queryAmfContextNon3gppRequest Nudr_DataRepository.QueryAmfContextNon3gppRequest queryAmfContextNon3gppRequest.SupportedFeatures = &supportedFeatures @@ -256,6 +272,23 @@ func (s *Server) HandleGetAmfNon3gppAccess(c *gin.Context) { func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { var amfNon3GppAccessRegistration models.AmfNon3GppAccessRegistration + ueID := c.Param("ueId") + // TS 29.503 5.3.2.2.3 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + requestBody, err := c.GetRawData() if err != nil { problemDetail := models.ProblemDetails{ @@ -286,14 +319,30 @@ func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { logger.UecmLog.Infof("Handle RegisterAmfNon3gppAccessRequest") - ueID := c.Param("ueId") - s.Processor().RegisterAmfNon3gppAccessProcedure(c, amfNon3GppAccessRegistration, ueID) } // RegistrationAmf3gppAccess - register as AMF for 3GPP access func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { var amf3GppAccessRegistration models.Amf3GppAccessRegistration + + ueID := c.Param("ueId") + // TS 29.503 5.3.2.2.2 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + requestBody, err := c.GetRawData() if err != nil { problemDetail := models.ProblemDetails{ @@ -348,7 +397,6 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { logger.UecmLog.Infof("Handle RegistrationAmf3gppAccess") - ueID := c.Param("ueId") logger.UecmLog.Info("UEID: ", ueID) s.Processor().RegistrationAmf3gppAccessProcedure(c, amf3GppAccessRegistration, ueID) @@ -357,6 +405,24 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { // UpdateAmfNon3gppAccess - update a parameter in the AMF registration for non-3GPP access func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { var amfNon3GppAccessRegistrationModification models.AmfNon3GppAccessRegistrationModification + + ueID := c.Param("ueId") + // TS 29.503 5.3.2.6.3 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + requestBody, err := c.GetRawData() if err != nil { problemDetail := models.ProblemDetails{ @@ -387,8 +453,6 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { logger.UecmLog.Infof("Handle UpdateAmfNon3gppAccessRequest") - ueID := c.Param("ueId") - s.Processor().UpdateAmfNon3gppAccessProcedure(c, amfNon3GppAccessRegistrationModification, ueID) } @@ -396,6 +460,23 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { func (s *Server) HandleUpdateAmf3gppAccess(c *gin.Context) { var amf3GppAccessRegistrationModification models.Amf3GppAccessRegistrationModification + ueID := c.Param("ueId") + // TS 29.503 5.3.2.6.2 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + requestBody, err := c.GetRawData() if err != nil { problemDetail := models.ProblemDetails{ @@ -426,8 +507,6 @@ func (s *Server) HandleUpdateAmf3gppAccess(c *gin.Context) { logger.UecmLog.Infof("Handle UpdateAmf3gppAccessRequest") - ueID := c.Param("ueId") - s.Processor().UpdateAmf3gppAccessProcedure(c, amf3GppAccessRegistrationModification, ueID) } @@ -466,6 +545,21 @@ func (s *Server) HandleDeregistrationSmfRegistrations(c *gin.Context) { logger.UecmLog.Infof("Handle DeregistrationSmfRegistrations") ueID := c.Params.ByName("ueId") + // TS 29.503 5.3.2.4.4 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } pduSessionID := c.Params.ByName("pduSessionId") s.Processor().DeregistrationSmfRegistrationsProcedure(c, ueID, pduSessionID) @@ -475,6 +569,23 @@ func (s *Server) HandleDeregistrationSmfRegistrations(c *gin.Context) { func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { var smfRegistration models.SmfRegistration + ueID := c.Params.ByName("ueId") + // TS 29.503 5.3.2.2.4 + // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + requestBody, err := c.GetRawData() if err != nil { problemDetail := models.ProblemDetails{ @@ -505,7 +616,6 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { logger.UecmLog.Infof("Handle RegistrationSmfRegistrations") - ueID := c.Params.ByName("ueId") pduSessionID := c.Params.ByName("pduSessionId") s.Processor().RegistrationSmfRegistrationsProcedure( @@ -521,6 +631,21 @@ func (s *Server) HandleGetAmf3gppAccess(c *gin.Context) { logger.UecmLog.Infof("Handle HandleGetAmf3gppAccessRequest") ueID := c.Param("ueId") + // TS 29.503 5.3.2.5.2 + // Validate SUPI and GPSI format the UE ID (SUPI or GPSI) shall be in the format defined in 3GPP TS 23.003 & 29.571 + valid := util.IsValidSupi(ueID) || util.IsValidGpsi(ueID) + if !valid { + problemDetail := models.ProblemDetails{ + Title: "Invalid ueID format", + Status: http.StatusBadRequest, + Detail: "The ueID format is invalid", + Cause: "INVALID_KEY", + } + logger.UecmLog.Warnf("Registration Reject: Invalid ueID format [%s]", ueID) + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } supportedFeatures := c.Query("supported-features") s.Processor().GetAmf3gppAccessProcedure(c, ueID, supportedFeatures) diff --git a/internal/util/gpsi.go b/internal/util/gpsi.go new file mode 100644 index 0000000..9e5cf4c --- /dev/null +++ b/internal/util/gpsi.go @@ -0,0 +1,33 @@ +package util + +import ( + "regexp" + "strings" +) +// TS 29.571 5.3.2 & TS 23.003 3.3 +// MSISDN format validation +var gpsiMsisdnRegex = regexp.MustCompile(`^msisdn-[0-9]{5,15}$`) +// TS 29.571 5.3.2 & TS 23.003 19.7.2 +// External Identifier format validation +var gpsiExtIdRegex = regexp.MustCompile(`^extid-.+@.+$`) + +// IsValidGpsi valid GPSI format (TS 29.571 5.3.2) +func IsValidGpsi(gpsi string) bool { + if len(gpsi) == 0 { + return false + } + + if strings.HasPrefix(gpsi, "msisdn-") { + return gpsiMsisdnRegex.MatchString(gpsi) + } + + if strings.HasPrefix(gpsi, "extid-") { + // External Identifier should not contain null byte + if strings.Contains(gpsi, "\x00") { + return false + } + return gpsiExtIdRegex.MatchString(gpsi) + } + + return false +} \ No newline at end of file diff --git a/internal/util/gpsi_test.go b/internal/util/gpsi_test.go new file mode 100644 index 0000000..33f9c5b --- /dev/null +++ b/internal/util/gpsi_test.go @@ -0,0 +1,70 @@ +package util + +import ( + "testing" +) + +func TestIsValidGpsi(t *testing.T) { + type args struct { + gpsi string + } + type testCase struct { + name string + args args + want bool + } + + runTests := func(t *testing.T, tests []testCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidGpsi(tt.args.gpsi); got != tt.want { + t.Errorf("IsValidGpsi() = %v, want %v (input: %q)", got, tt.want, tt.args.gpsi) + } + }) + } + } + + // MSISDN test + t.Run("Check_MSISDN", func(t *testing.T) { + tests := []testCase{ + {"Valid MSISDN (Standard)", args{"msisdn-886912345678"}, true}, + {"Valid MSISDN (Min Length 5)", args{"msisdn-12345"}, true}, // TS 29.571 regex lower bound + {"Valid MSISDN (Max Length 15)", args{"msisdn-123456789012345"}, true}, // TS 29.571 regex upper bound + {"Invalid MSISDN (Too short)", args{"msisdn-1234"}, false}, + {"Invalid MSISDN (Too long)", args{"msisdn-1234567890123456"}, false}, + {"Invalid MSISDN (Non-digits)", args{"msisdn-886912abc"}, false}, + {"Invalid MSISDN (Null Byte)", args{"msisdn-123\x00456"}, false}, + } + runTests(t, tests) + }) + + // external identifier test + t.Run("Check_ExternalID", func(t *testing.T) { + tests := []testCase{ + {"Valid ExtID (Standard)", args{"extid-user@domain.com"}, true}, + {"Valid ExtID (Subdomain)", args{"extid-sensor1@factory.iot.org"}, true}, + {"Invalid ExtID (Missing @)", args{"extid-user-domain.com"}, false}, // TS 23.003: MUST have @ + {"Invalid ExtID (Missing LocalId)", args{"extid-@domain.com"}, false}, + {"Invalid ExtID (Missing DomainId)", args{"extid-user@"}, false}, + {"Invalid ExtID (Empty content)", args{"extid-"}, false}, + // [Security] Null Byte Injection check + {"Security: ExtID with Null Byte", args{"extid-user\x00@domain.com"}, false}, + } + runTests(t, tests) + }) + + // other invalid cases + t.Run("Check_General_Invalid", func(t *testing.T) { + tests := []testCase{ + {"Empty String", args{""}, false}, + {"Unknown Prefix", args{"unknown-12345"}, false}, + {"Just Prefix (msisdn-)", args{"msisdn-"}, false}, + {"Just Prefix (extid-)", args{"extid-"}, false}, + // [Security] Fuzzing / Garbage + {"Security: Raw Null Bytes", args{"\x00\x00\x00"}, false}, + {"Security: Random String", args{"not_a_gpsi"}, false}, + } + runTests(t, tests) + }) +} \ No newline at end of file diff --git a/internal/util/supi.go b/internal/util/supi.go new file mode 100644 index 0000000..e178947 --- /dev/null +++ b/internal/util/supi.go @@ -0,0 +1,39 @@ +package util + +import ( + "regexp" + "strings" +) + +// TS 29.571 5.3.2 & TS 23.003 +// SUPI format validation +var supiImsiRegex = regexp.MustCompile(`^imsi-[0-9]{5,15}$`) +// TS 29.571 5.3.2 & TS 23.003 28.7.2 +// NAI format validation +var supiNaiRegex = regexp.MustCompile(`^nai-.+@.+$`) +// TS 29.571 5.3.2 & TS 23.003 28.15.2(gci) & TS 23.003 28.16.2(gli) +// GCI/GLI format validation +var supiGciGliRegex = regexp.MustCompile(`^(gci|gli)-.+$`) + +// IsValidSupi checks if the given SUPI is valid according to 3GPP specifications +func IsValidSupi(supi string) bool { + if len(supi) == 0 { + return false + } + + if strings.HasPrefix(supi, "imsi-") { + return supiImsiRegex.MatchString(supi) + } + + if strings.HasPrefix(supi, "nai-") { + if strings.Contains(supi, "\x00") { + return false + } + return supiNaiRegex.MatchString(supi) + } + if strings.HasPrefix(supi, "gci-") || strings.HasPrefix(supi, "gli-") { + return supiGciGliRegex.MatchString(supi) + } + + return false +} \ No newline at end of file diff --git a/internal/util/supi_test.go b/internal/util/supi_test.go new file mode 100644 index 0000000..2167a01 --- /dev/null +++ b/internal/util/supi_test.go @@ -0,0 +1,76 @@ +package util + +import ( + "testing" +) + +func TestIsValidSupi(t *testing.T) { + type Args struct { + supi string + } + + type testCase struct { + name string + Args Args + Want bool + } + runTests := func(t *testing.T, tests []testCase) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsValidSupi(tt.Args.supi); got != tt.Want { + t.Errorf("IsValidSupi() = %v, Want %v (input: %q)", got, tt.Want, tt.Args.supi) + } + }) + } + } + // IMSI test + t.Run("Check_IMSI", func(t *testing.T) { + tests := []testCase{ + {"Valid IMSI (15 digits)", Args{"imsi-208930000000003"}, true}, + {"Valid IMSI (5 digits)", Args{"imsi-12345"}, true}, + {"Invalid IMSI (Too short)", Args{"imsi-1234"}, false}, + {"Invalid IMSI (Too long)", Args{"imsi-1234567890123456"}, false}, + {"Invalid IMSI (Non-digits)", Args{"imsi-20893abc000003"}, false}, + {"Invalid IMSI (Null Byte)", Args{"imsi-123\x00456"}, false}, + } + runTests(t, tests) + }) + + // NAI test + t.Run("Check_NAI", func(t *testing.T) { + tests := []testCase{ + {"Valid NAI (Standard)", Args{"nai-user@realm.com"}, true}, + {"Valid NAI (3GPP Style)", Args{"nai-type0.rid123.schid0.userid1@5gc.mnc001.mcc001.org"}, true}, + {"Invalid NAI (Missing @)", Args{"nai-userrealm.com"}, false}, + {"Invalid NAI (Missing Realm)", Args{"nai-user@"}, false}, + {"Invalid NAI (Missing User)", Args{"nai-@realm"}, false}, + {"Security: NAI with Null Byte", Args{"nai-user\x00@realm"}, false}, + } + runTests(t, tests) + }) + + // GCI/GLI test + t.Run("Check_GCI_GLI", func(t *testing.T) { + tests := []testCase{ + {"Valid GCI", Args{"gci-cable-mac-1234"}, true}, + {"Valid GLI", Args{"gli-fiber-line-5678"}, true}, + {"Invalid GCI (Empty body)", Args{"gci-"}, false}, + {"Invalid GLI (Empty body)", Args{"gli-"}, false}, + } + runTests(t, tests) + }) + + // General invalid test + t.Run("Check_General_Invalid", func(t *testing.T) { + tests := []testCase{ + {"Empty String", Args{""}, false}, + {"Unknown Prefix", Args{"unknown-12345"}, false}, + {"Just Prefix", Args{"imsi-"}, false}, + {"Security: Raw Null Bytes", Args{"\x00\x00\x00"}, false}, + {"Security: Garbage", Args{"fuzzing_payload"}, false}, + } + runTests(t, tests) + }) + +} \ No newline at end of file From 85ddad8609680f4ab212a6d3e79df5bb026cb899 Mon Sep 17 00:00:00 2001 From: chchen7 Date: Mon, 29 Dec 2025 01:51:34 +0800 Subject: [PATCH 3/6] fix: more uecm mandatory IE check --- internal/sbi/api_uecontextmanagement.go | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index 8c4c622..5629cce 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -317,6 +317,31 @@ func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { return } + // TS 29.503 6.2.6.2.3 requirements check + missingIE := "" + if amfNon3GppAccessRegistration.AmfInstanceId == "" { + missingIE = "AmfInstanceId" + } else if amfNon3GppAccessRegistration.Guami == nil { + missingIE = "Guami" + } else if amfNon3GppAccessRegistration.DeregCallbackUri == "" { + missingIE = "DeregCallbackUri" + } else if amfNon3GppAccessRegistration.RatType == "" { + missingIE = "RatType" + } + + if missingIE != "" { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE " + missingIE + " is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + logger.UecmLog.Infof("Handle RegisterAmfNon3gppAccessRequest") s.Processor().RegisterAmfNon3gppAccessProcedure(c, amfNon3GppAccessRegistration, ueID) @@ -451,6 +476,25 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { return } + // TS 29.503 6.2.6.2.8 requirements check + missingIE := "" + if amfNon3GppAccessRegistrationModification.Guami == nil { + missingIE = "Guami" + } + + if missingIE != "" { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE " + missingIE + " is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + logger.UecmLog.Infof("Handle UpdateAmfNon3gppAccessRequest") s.Processor().UpdateAmfNon3gppAccessProcedure(c, amfNon3GppAccessRegistrationModification, ueID) @@ -505,6 +549,25 @@ func (s *Server) HandleUpdateAmf3gppAccess(c *gin.Context) { return } + // TS 29.503 6.2.6.2.7 requirements check + missingIE := "" + if amf3GppAccessRegistrationModification.Guami == nil { + missingIE = "Guami" + } + + if missingIE != "" { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE " + missingIE + " is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + logger.UecmLog.Infof("Handle UpdateAmf3gppAccessRequest") s.Processor().UpdateAmf3gppAccessProcedure(c, amf3GppAccessRegistrationModification, ueID) @@ -614,6 +677,25 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { return } + // TS 29.503 6.2.6.2.4 requirements check + missingIE := "" + if smfRegistration.SmfInstanceId == "" { + missingIE = "SmfInstanceId" + } + + if missingIE != "" { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE " + missingIE + " is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } + logger.UecmLog.Infof("Handle RegistrationSmfRegistrations") pduSessionID := c.Params.ByName("pduSessionId") From b8317bacbd076bd7610c602defcc897da5573c71 Mon Sep 17 00:00:00 2001 From: chchen7 Date: Mon, 29 Dec 2025 02:13:36 +0800 Subject: [PATCH 4/6] fix: validate pduDessionID --- internal/sbi/api_uecontextmanagement.go | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index 5629cce..daa4912 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -2,6 +2,7 @@ package sbi import ( "net/http" + "strconv" "github.com/gin-gonic/gin" @@ -624,6 +625,20 @@ func (s *Server) HandleDeregistrationSmfRegistrations(c *gin.Context) { return } pduSessionID := c.Params.ByName("pduSessionId") + // TS 29.571 5.4.2 valid PDU Session ID is an integer in the range 0 to 255 + pduSessionIDInt,err := strconv.Atoi(pduSessionID) + if pduSessionIDInt < 0 || pduSessionIDInt > 255 || err != nil { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE PduSessionId is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE PduSessionId is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } s.Processor().DeregistrationSmfRegistrationsProcedure(c, ueID, pduSessionID) } @@ -699,6 +714,20 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { logger.UecmLog.Infof("Handle RegistrationSmfRegistrations") pduSessionID := c.Params.ByName("pduSessionId") + // TS 29.571 5.4.2 valid PDU Session ID is an integer in the range 0 to 255 + pduSessionIDInt,err := strconv.Atoi(pduSessionID) + if pduSessionIDInt < 0 || pduSessionIDInt > 255 || err != nil { + problemDetail := models.ProblemDetails{ + Title: "Missing or invalid parameter", + Status: http.StatusBadRequest, + Detail: "Mandatory IE PduSessionId is missing or invalid", + Cause: "MISSING_OR_INVALID_PARAMETER", + } + logger.UecmLog.Errorln("Mandatory IE PduSessionId is missing or invalid") + c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) + c.JSON(int(problemDetail.Status), problemDetail) + return + } s.Processor().RegistrationSmfRegistrationsProcedure( c, From 04a1f515f4163a2ca66717b4fc105e2aa29924bf Mon Sep 17 00:00:00 2001 From: chchen7 Date: Mon, 29 Dec 2025 02:31:01 +0800 Subject: [PATCH 5/6] refactor: downgrade validation logs from Error to Warn --- internal/sbi/api_uecontextmanagement.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index daa4912..569357a 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -337,7 +337,7 @@ func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { Detail: "Mandatory IE " + missingIE + " is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE " + missingIE + " is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -415,7 +415,7 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { Detail: "Mandatory IE " + missingIE + " is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE " + missingIE + " is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -490,7 +490,7 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { Detail: "Mandatory IE " + missingIE + " is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE " + missingIE + " is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -563,7 +563,7 @@ func (s *Server) HandleUpdateAmf3gppAccess(c *gin.Context) { Detail: "Mandatory IE " + missingIE + " is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE " + missingIE + " is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -634,7 +634,7 @@ func (s *Server) HandleDeregistrationSmfRegistrations(c *gin.Context) { Detail: "Mandatory IE PduSessionId is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE PduSessionId is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE PduSessionId is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -705,7 +705,7 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { Detail: "Mandatory IE " + missingIE + " is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE " + missingIE + " is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE " + missingIE + " is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return @@ -723,7 +723,7 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { Detail: "Mandatory IE PduSessionId is missing or invalid", Cause: "MISSING_OR_INVALID_PARAMETER", } - logger.UecmLog.Errorln("Mandatory IE PduSessionId is missing or invalid") + logger.UecmLog.Warnln("Mandatory IE PduSessionId is missing or invalid") c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return From e0a269dcf516b83a3da80444cea9cdc23ea52f4a Mon Sep 17 00:00:00 2001 From: chchen7 Date: Mon, 29 Dec 2025 03:27:58 +0800 Subject: [PATCH 6/6] chore: apply golangci-lint fixes --- internal/sbi/api_uecontextmanagement.go | 66 ++++++++++++------------- internal/util/gpsi.go | 34 +++++++------ internal/util/gpsi_test.go | 4 +- internal/util/supi.go | 10 ++-- internal/util/supi_test.go | 3 +- 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/internal/sbi/api_uecontextmanagement.go b/internal/sbi/api_uecontextmanagement.go index 569357a..b6bfe66 100644 --- a/internal/sbi/api_uecontextmanagement.go +++ b/internal/sbi/api_uecontextmanagement.go @@ -10,8 +10,8 @@ import ( "github.com/free5gc/openapi/models" Nudr_DataRepository "github.com/free5gc/openapi/udr/DataRepository" "github.com/free5gc/udm/internal/logger" - "github.com/free5gc/util/metrics/sbi" "github.com/free5gc/udm/internal/util" + "github.com/free5gc/util/metrics/sbi" ) func (s *Server) getUEContextManagementRoutes() []Route { @@ -320,15 +320,15 @@ func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { // TS 29.503 6.2.6.2.3 requirements check missingIE := "" - if amfNon3GppAccessRegistration.AmfInstanceId == "" { - missingIE = "AmfInstanceId" - } else if amfNon3GppAccessRegistration.Guami == nil { - missingIE = "Guami" - } else if amfNon3GppAccessRegistration.DeregCallbackUri == "" { - missingIE = "DeregCallbackUri" - } else if amfNon3GppAccessRegistration.RatType == "" { - missingIE = "RatType" - } + if amfNon3GppAccessRegistration.AmfInstanceId == "" { + missingIE = "AmfInstanceId" + } else if amfNon3GppAccessRegistration.Guami == nil { + missingIE = "Guami" + } else if amfNon3GppAccessRegistration.DeregCallbackUri == "" { + missingIE = "DeregCallbackUri" + } else if amfNon3GppAccessRegistration.RatType == "" { + missingIE = "RatType" + } if missingIE != "" { problemDetail := models.ProblemDetails{ @@ -341,7 +341,7 @@ func (s *Server) HandleRegistrationAmfNon3gppAccess(c *gin.Context) { c.Set(sbi.IN_PB_DETAILS_CTX_STR, http.StatusText(int(problemDetail.Status))) c.JSON(int(problemDetail.Status), problemDetail) return - } + } logger.UecmLog.Infof("Handle RegisterAmfNon3gppAccessRequest") @@ -353,7 +353,7 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { var amf3GppAccessRegistration models.Amf3GppAccessRegistration ueID := c.Param("ueId") - // TS 29.503 5.3.2.2.2 + // TS 29.503 5.3.2.2.2 // Validate SUPI format the UE ID (SUPI) shall be in the format defined in 3GPP TS 23.003 & 29.571 valid := util.IsValidSupi(ueID) if !valid { @@ -398,15 +398,15 @@ func (s *Server) HandleRegistrationAmf3gppAccess(c *gin.Context) { } // TS 29.503 6.2.6.2.2 requirements check missingIE := "" - if amf3GppAccessRegistration.AmfInstanceId == "" { - missingIE = "AmfInstanceId" - } else if amf3GppAccessRegistration.Guami == nil { - missingIE = "Guami" - } else if amf3GppAccessRegistration.DeregCallbackUri == "" { - missingIE = "DeregCallbackUri" - } else if amf3GppAccessRegistration.RatType == "" { - missingIE = "RatType" - } + if amf3GppAccessRegistration.AmfInstanceId == "" { + missingIE = "AmfInstanceId" + } else if amf3GppAccessRegistration.Guami == nil { + missingIE = "Guami" + } else if amf3GppAccessRegistration.DeregCallbackUri == "" { + missingIE = "DeregCallbackUri" + } else if amf3GppAccessRegistration.RatType == "" { + missingIE = "RatType" + } if missingIE != "" { problemDetail := models.ProblemDetails{ @@ -479,9 +479,9 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { // TS 29.503 6.2.6.2.8 requirements check missingIE := "" - if amfNon3GppAccessRegistrationModification.Guami == nil { - missingIE = "Guami" - } + if amfNon3GppAccessRegistrationModification.Guami == nil { + missingIE = "Guami" + } if missingIE != "" { problemDetail := models.ProblemDetails{ @@ -495,7 +495,7 @@ func (s *Server) HandleUpdateAmfNon3gppAccess(c *gin.Context) { c.JSON(int(problemDetail.Status), problemDetail) return } - + logger.UecmLog.Infof("Handle UpdateAmfNon3gppAccessRequest") s.Processor().UpdateAmfNon3gppAccessProcedure(c, amfNon3GppAccessRegistrationModification, ueID) @@ -552,9 +552,9 @@ func (s *Server) HandleUpdateAmf3gppAccess(c *gin.Context) { // TS 29.503 6.2.6.2.7 requirements check missingIE := "" - if amf3GppAccessRegistrationModification.Guami == nil { - missingIE = "Guami" - } + if amf3GppAccessRegistrationModification.Guami == nil { + missingIE = "Guami" + } if missingIE != "" { problemDetail := models.ProblemDetails{ @@ -626,7 +626,7 @@ func (s *Server) HandleDeregistrationSmfRegistrations(c *gin.Context) { } pduSessionID := c.Params.ByName("pduSessionId") // TS 29.571 5.4.2 valid PDU Session ID is an integer in the range 0 to 255 - pduSessionIDInt,err := strconv.Atoi(pduSessionID) + pduSessionIDInt, err := strconv.Atoi(pduSessionID) if pduSessionIDInt < 0 || pduSessionIDInt > 255 || err != nil { problemDetail := models.ProblemDetails{ Title: "Missing or invalid parameter", @@ -694,9 +694,9 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { // TS 29.503 6.2.6.2.4 requirements check missingIE := "" - if smfRegistration.SmfInstanceId == "" { - missingIE = "SmfInstanceId" - } + if smfRegistration.SmfInstanceId == "" { + missingIE = "SmfInstanceId" + } if missingIE != "" { problemDetail := models.ProblemDetails{ @@ -715,7 +715,7 @@ func (s *Server) HandleRegistrationSmfRegistrations(c *gin.Context) { pduSessionID := c.Params.ByName("pduSessionId") // TS 29.571 5.4.2 valid PDU Session ID is an integer in the range 0 to 255 - pduSessionIDInt,err := strconv.Atoi(pduSessionID) + pduSessionIDInt, err := strconv.Atoi(pduSessionID) if pduSessionIDInt < 0 || pduSessionIDInt > 255 || err != nil { problemDetail := models.ProblemDetails{ Title: "Missing or invalid parameter", diff --git a/internal/util/gpsi.go b/internal/util/gpsi.go index 9e5cf4c..45b9e5d 100644 --- a/internal/util/gpsi.go +++ b/internal/util/gpsi.go @@ -4,30 +4,32 @@ import ( "regexp" "strings" ) + // TS 29.571 5.3.2 & TS 23.003 3.3 // MSISDN format validation var gpsiMsisdnRegex = regexp.MustCompile(`^msisdn-[0-9]{5,15}$`) + // TS 29.571 5.3.2 & TS 23.003 19.7.2 // External Identifier format validation -var gpsiExtIdRegex = regexp.MustCompile(`^extid-.+@.+$`) +var gpsiExtIdRegex = regexp.MustCompile(`^extid-.+@.+$`) // IsValidGpsi valid GPSI format (TS 29.571 5.3.2) func IsValidGpsi(gpsi string) bool { - if len(gpsi) == 0 { - return false - } + if len(gpsi) == 0 { + return false + } - if strings.HasPrefix(gpsi, "msisdn-") { - return gpsiMsisdnRegex.MatchString(gpsi) - } + if strings.HasPrefix(gpsi, "msisdn-") { + return gpsiMsisdnRegex.MatchString(gpsi) + } - if strings.HasPrefix(gpsi, "extid-") { - // External Identifier should not contain null byte - if strings.Contains(gpsi, "\x00") { - return false - } - return gpsiExtIdRegex.MatchString(gpsi) - } + if strings.HasPrefix(gpsi, "extid-") { + // External Identifier should not contain null byte + if strings.Contains(gpsi, "\x00") { + return false + } + return gpsiExtIdRegex.MatchString(gpsi) + } - return false -} \ No newline at end of file + return false +} diff --git a/internal/util/gpsi_test.go b/internal/util/gpsi_test.go index 33f9c5b..7e0e1f1 100644 --- a/internal/util/gpsi_test.go +++ b/internal/util/gpsi_test.go @@ -29,7 +29,7 @@ func TestIsValidGpsi(t *testing.T) { t.Run("Check_MSISDN", func(t *testing.T) { tests := []testCase{ {"Valid MSISDN (Standard)", args{"msisdn-886912345678"}, true}, - {"Valid MSISDN (Min Length 5)", args{"msisdn-12345"}, true}, // TS 29.571 regex lower bound + {"Valid MSISDN (Min Length 5)", args{"msisdn-12345"}, true}, // TS 29.571 regex lower bound {"Valid MSISDN (Max Length 15)", args{"msisdn-123456789012345"}, true}, // TS 29.571 regex upper bound {"Invalid MSISDN (Too short)", args{"msisdn-1234"}, false}, {"Invalid MSISDN (Too long)", args{"msisdn-1234567890123456"}, false}, @@ -67,4 +67,4 @@ func TestIsValidGpsi(t *testing.T) { } runTests(t, tests) }) -} \ No newline at end of file +} diff --git a/internal/util/supi.go b/internal/util/supi.go index e178947..cfaf078 100644 --- a/internal/util/supi.go +++ b/internal/util/supi.go @@ -8,9 +8,11 @@ import ( // TS 29.571 5.3.2 & TS 23.003 // SUPI format validation var supiImsiRegex = regexp.MustCompile(`^imsi-[0-9]{5,15}$`) -// TS 29.571 5.3.2 & TS 23.003 28.7.2 + +// TS 29.571 5.3.2 & TS 23.003 28.7.2 // NAI format validation var supiNaiRegex = regexp.MustCompile(`^nai-.+@.+$`) + // TS 29.571 5.3.2 & TS 23.003 28.15.2(gci) & TS 23.003 28.16.2(gli) // GCI/GLI format validation var supiGciGliRegex = regexp.MustCompile(`^(gci|gli)-.+$`) @@ -27,8 +29,8 @@ func IsValidSupi(supi string) bool { if strings.HasPrefix(supi, "nai-") { if strings.Contains(supi, "\x00") { - return false - } + return false + } return supiNaiRegex.MatchString(supi) } if strings.HasPrefix(supi, "gci-") || strings.HasPrefix(supi, "gli-") { @@ -36,4 +38,4 @@ func IsValidSupi(supi string) bool { } return false -} \ No newline at end of file +} diff --git a/internal/util/supi_test.go b/internal/util/supi_test.go index 2167a01..fdb7627 100644 --- a/internal/util/supi_test.go +++ b/internal/util/supi_test.go @@ -72,5 +72,4 @@ func TestIsValidSupi(t *testing.T) { } runTests(t, tests) }) - -} \ No newline at end of file +}