nebari-dev / nebari-dev/data-science-pack

Security(M-05): Nebi workspace selection is shell-interpolated and init failures are masked

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

Nobody has claimed this yet.

area: security priority: medium ⚡
Dominant language
Python
Stars
5
Forks
7
Avg merge
1d 20h
Merged PRs (30d)
13

Description

Summary

A user-selected Nebi workspace value is inserted into a shell command that runs nebi pull and pixi install. The value is interpolated into an sh -c string rather than passed as an argument array, and any failure in the chain is downgraded to a warning so the pod continues after a failed or partial environment operation. The init container also holds a short-lived Nebi credential and has no explicit security context in this custom code.

Severity: Medium · CWE-78 (OS Command Injection)
Validation: Confirmed against HEAD f932d80 on 2026-07-14 (assessed at 69c84f7; unchanged since).

Evidence

  • User-controlled value: config/jupyterhub/01-spawner.py:692-694 reads conda_env from spawner.user_options and derives workspace_name via rsplit, with no validation anywhere in config/jupyterhub/.
  • Shell interpolation + masked failure: 01-spawner.py:751-761 builds /bin/sh -c "mkdir -p {ws_dir} && nebi pull {workspace_name} -o {ws_dir} --force && pixi install ... || echo 'WARNING: ...'". The trailing || echo catches any failure in the && chain and exits 0, so the init container succeeds even when the environment operation failed.
  • Credential in init env: 01-spawner.py:719-722 sets NEBI_AUTH_TOKEN to the exchanged JWT.
  • No explicit security context: the nebi-pull init container dict (01-spawner.py:747-764) has no securityContext; the pod-level context (01-spawner.py:133) sets only fsGroup. This container runs as the pod default (non-root), so injected code runs at the user's own identity, not root.

Impact

The user already has code-execution authority in their own environment, so this is not an independent tenant escape. It does broaden command interpretation before the intended environment starts, places a credential in a shell-driven path, hides integrity failures, and undermines reproducibility. A user_options value like foo;<cmd> (no slash) survives rsplit intact into the shell string.

Remediation

  • Validate workspace IDs against a strict grammar.
  • Pass arguments without a shell.
  • Give the init container an explicit restricted security context.
  • Use a single-purpose, workspace-bound token.
  • Fail the spawn when pull or install fails, rather than logging a warning and continuing.

Acceptance criteria

  • Shell metacharacters cannot change execution.
  • Failed initialization prevents readiness.
  • The token cannot access another user's workspace.

Source: data-science-pack 0.1.0 security assessment (pinned commit 69c84f72df259ec755ed40bfc83f20158c550d55), finding M-05.

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.

Research direction

Start in config/jupyterhub/01-spawner.py at lines 692-694 and trace the nebi-pull init container setup at lines 719-722 and 747-764. Review how user_options reaches the shell command, how failures affect readiness, and how the pod security context is applied. Done means metacharacters cannot alter execution, initialization failures prevent readiness, and the token is workspace-bound.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, shell
Domain
infrastructure, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.