Summary
The spawner interpolates Keycloak group names, unquoted, into a shell command that runs as root in an init container. There is no safe-character validation, so shell metacharacters in a group name alter command execution.
Severity: Medium · CWE-78 (OS Command Injection)
Validation: Confirmed against HEAD f932d80 on 2026-07-14 (assessed at 69c84f7; unchanged since).
Evidence
config/jupyterhub/01-spawner.py:858-861 builds mkdir -p /mnt/shared/{g} && chown 0:100 /mnt/shared/{g} && chmod 2775 /mnt/shared/{g} for each group with {g} unquoted, joins them, and at 01-spawner.py:865-866 runs them via ["sh", "-c", mkdir_cmds] with securityContext: {runAsUser: 0}. Line 866 is the only runAsUser: 0 in the file.
- A postStart hook does the same for
/home/jovyan/shared/{group}: 01-spawner.py:989 (unquoted) joined into ["/bin/sh", "-c", ...] at 01-spawner.py:1004. This one runs in the singleuser container as the notebook user, not root, so its impact is lower than the init container.
- No sanitization: the only transform on group names is
Path(g).name (01-spawner.py:821), which is a path-leaf extractor, not a shell escaper. escapism.escape exists in the file (01-spawner.py:1022) but is used only for the PVC-name slug. For contrast, usernames written to passwd/group files are safely quoted with Python repr() (01-spawner.py:938-941), so the group-name path is an inconsistency.
Impact
An actor who can create, rename, or influence a Keycloak group and place a user in it can execute arbitrary shell syntax in the init container as root, modify shared data, and manipulate startup state. The precondition is control of a group name, which is normally a delegated-admin or IdP operation, so this is an admin-or-IdP-to-root escalation rather than a self-service user path.
Remediation
- Derive a strict filesystem identifier from an immutable group ID.
- Use filesystem APIs or argument arrays rather than a shell string.
- Reject unsafe historical group names.
- Give the init container a non-root, minimal-capability design.
Acceptance criteria
Source: data-science-pack 0.1.0 security assessment (pinned commit 69c84f72df259ec755ed40bfc83f20158c550d55), finding M-01.
Summary
The spawner interpolates Keycloak group names, unquoted, into a shell command that runs as root in an init container. There is no safe-character validation, so shell metacharacters in a group name alter command execution.
Severity: Medium · CWE-78 (OS Command Injection)
Validation: Confirmed against HEAD
f932d80on 2026-07-14 (assessed at69c84f7; unchanged since).Evidence
config/jupyterhub/01-spawner.py:858-861buildsmkdir -p /mnt/shared/{g} && chown 0:100 /mnt/shared/{g} && chmod 2775 /mnt/shared/{g}for each group with{g}unquoted, joins them, and at01-spawner.py:865-866runs them via["sh", "-c", mkdir_cmds]withsecurityContext: {runAsUser: 0}. Line 866 is the onlyrunAsUser: 0in the file./home/jovyan/shared/{group}:01-spawner.py:989(unquoted) joined into["/bin/sh", "-c", ...]at01-spawner.py:1004. This one runs in the singleuser container as the notebook user, not root, so its impact is lower than the init container.Path(g).name(01-spawner.py:821), which is a path-leaf extractor, not a shell escaper.escapism.escapeexists in the file (01-spawner.py:1022) but is used only for the PVC-name slug. For contrast, usernames written to passwd/group files are safely quoted with Pythonrepr()(01-spawner.py:938-941), so the group-name path is an inconsistency.Impact
An actor who can create, rename, or influence a Keycloak group and place a user in it can execute arbitrary shell syntax in the init container as root, modify shared data, and manipulate startup state. The precondition is control of a group name, which is normally a delegated-admin or IdP operation, so this is an admin-or-IdP-to-root escalation rather than a self-service user path.
Remediation
Acceptance criteria
sh -cstring.Source: data-science-pack 0.1.0 security assessment (pinned commit
69c84f72df259ec755ed40bfc83f20158c550d55), finding M-01.