Management REST API binds all interfaces with no authentication; app load path runs unverified bytes
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 35/100
Research direction
Read the REST router and bind setup in src/rest_api_lib/src/lib.rs, then trace POST /app and DELETE /app/{id} through src/controller/jrtc_int.c and the Python loader paths. Run the supplied curl check against a controller instance and verify the completed behavior includes loopback-safe defaults, deliberate authenticated remote access, protected app loading, and rejected out-of-range IDs.
Written by the indexing model from the issue text.
Description
Summary
The management REST API defaults to 0.0.0.0:3001 with no authentication on any route, and POST /app feeds caller-supplied bytes straight into a dlopen inside the controller process. For the documented single-host usage (jrtc-ctl against localhost:3001) nothing is exposed; but the defaults an integrator inherits before wiring a management framework are bind-all-interfaces with anonymous app loading. Two asks: safest-by-default binding plus an optional auth token — and one small memory-safety fix found next to it.
1. Unauthenticated management API on all interfaces, feeding an unsigned native-code load path
In src/rest_api_lib/src/lib.rs the router has no authentication or authorization middleware on any route, and the server binds every interface:
let app = Router::new()
.route("/app", get(get_apps).post(load_app))
.route("/app/:id", get(get_app).delete(unload_app))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
load_app takes the base64 app bytes from POST /app and passes them to the C load_app callback (src/controller/jrtc_int.c L300-335), which writes them to a memfd and calls dlopen("/proc/self/fd/N", RTLD_LAZY) (_jrtc_load_app_from_memory, src/controller/jrtc_int.c L149-176). There is no signature, hash, or allowlist check anywhere on this path.
Consequence: any host that can reach port 3001 can load arbitrary native code into the controller process — the process holding the jbpf shared-memory telemetry and control channels to the instrumented network functions. On a testbed where the controller and the RAN workloads share a bridge network, that is every peer on the bridge. The app_type: "python" route reaches a similar outcome: REST-supplied paths are appended to sys.path and REST-supplied modules are imported (src/pythonapp_loader/jrtc_pythonapp_loader.c L161-169, L357-367).
The README positions the management plane as integrator territory — jrtc-ctl "can be optionally integrated with other management frameworks (e.g. nRT-RIC or SMO in O-RAN terminology)". That is exactly why the built-in defaults should be safe without such a framework: a deployment that has not integrated one inherits today's bind-everywhere, no-auth defaults, and startup prints listening on 0.0.0.0:3001 with no indication that the port loads native code.
Minimal check (observed from source; runs in seconds): start the controller on host A, then from host B on the same network — no credential required, the shared library is loaded into the controller process:
curl -s -X POST http://A:3001/app -H 'content-type: application/json' \
-d '{"app":"<base64 of a .so exporting the jrtc app entry points>","app_name":"probe","runtime_us":1000,"deadline_us":0,"period_us":1000,"ioq_size":16,"app_path":"/tmp/probe","app_type":"binary","app_params":{},"device_mapping":{},"app_modules":[]}'
Expected: loopback by default, and remote management possible only after the integrator turns it on deliberately.
Suggested fixes:
- Default the REST bind to
127.0.0.1, with a configurable bind address for integrators who want remote management. - Add optional shared-token authentication (e.g. a bearer token from an environment variable or the config file) enforced as axum middleware, so a remote bind never means anonymous app loading.
- Optionally verify app payloads against an integrator-configured hash allowlist before
dlopen, so only artifacts the integrator has blessed can be loaded.
2. unload_app indexes app_envs[app_id] with no bounds check
DELETE /app/{id} passes the raw i32 path segment to the C unload_app (src/controller/jrtc_int.c L396-400), which indexes the fixed-size global array struct jrtc_app_env* app_envs[MAX_NUM_JRTC_APPS] (declared at L95) with no range check — a negative or large id reads out of bounds. A one-line guard closes it:
if (app_id < 0 || app_id >= MAX_NUM_JRTC_APPS) return -1;
and the REST layer could map out-of-range ids to a 400 response.
Environment
jrt-controller @ f18345e0fb23eb5bebdfead10b1335678c9dcaf2 (main, 2026-09-15). Findings from source review; the curl one-liner above is the minimal runtime check.
Happy to send PRs for any of these if the approach sounds right.
- Dominant language
- C
- Stars
- 18
- Forks
- 13
- Avg merge
- 22d 13h
- Merged PRs (30d)
- 1
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/jrt-controller
-
Difficulty 2/5 1-3 hours Newbie friendliness 55/100
microsoft/jrt-controller#86 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 30/100
microsoft/jrt-controller#83 ·
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
microsoft/jrt-controller#82 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 25/100
microsoft/jrt-controller#70 ·
-
microsoft/jrt-controller#62 · 1 assignee ·
All issues in microsoft/jrt-controller
Similar issues
-
os:linux
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
mpv-player/mpv#18510 · 2 comments ·
-
internal.h中,漏掉了1个定义。 Open
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
kovidgoyal/kitty#10516 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 80/100