nebari-dev / nebari-dev/data-science-pack
Security(M-05): Nebi workspace selection is shell-interpolated and init failures are masked
Nobody has claimed this yet.
- 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-694readsconda_envfromspawner.user_optionsand derivesworkspace_nameviarsplit, with no validation anywhere inconfig/jupyterhub/. - Shell interpolation + masked failure:
01-spawner.py:751-761builds/bin/sh -c "mkdir -p {ws_dir} && nebi pull {workspace_name} -o {ws_dir} --force && pixi install ... || echo 'WARNING: ...'". The trailing|| echocatches 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-722setsNEBI_AUTH_TOKENto the exchanged JWT. - No explicit security context: the nebi-pull init container dict (
01-spawner.py:747-764) has nosecurityContext; the pod-level context (01-spawner.py:133) sets onlyfsGroup. 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
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.
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