Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Management REST API binds all interfaces with no authentication; app load path runs unverified bytes

Open
#96 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
c, rust
Domain
api, backend, security

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from microsoft/jrt-controller

All issues in microsoft/jrt-controller

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.