Safeguard
Application Security

The Notebook Nobody Reviewed Is Running on a Schedule

It pulls customer records, holds a password in cell four, installs packages at runtime, and has run nightly for eighteen months. The format hides both the state and the data, and nothing gated the moment it became infrastructure.

Vikram Iyer
Security Researcher
6 min read

Notebooks are treated as scratch space and used as production. Somewhere in your company there is a .ipynb that pulls customer records, holds a database password in cell four, installs packages at runtime, and runs on a schedule because someone wired it to a job runner eighteen months ago.

It has never been code reviewed. It is not in your dependency scanning. Its outputs are committed to git. This post is the specific risks that come with the format, and what to do that does not involve telling data scientists to stop using notebooks. For whoever owns security for a team doing analysis or ML.

What makes the format different

Four properties, each reasonable in isolation.

Execution state is invisible and out of order. A cell that created a variable may have been deleted after running. The file you read is not the program that ran, and running it top to bottom may not reproduce the behaviour. This defeats reading the code to understand what it does.

Outputs are stored in the file. A notebook saves the results of every cell, so a .ipynb committed to git can contain query results, customer records, API responses and tokens printed during debugging. The data is in the repository, in a JSON blob nobody reads in review.

Dependencies are installed at runtime. !pip install somepackage inside a cell means the dependency is not in any manifest, not in your lockfile, and not in anything that scans manifests. It is invisible to your entire supply chain programme.

Credentials are typed inline. Because the alternative is configuring a secrets manager in an exploratory environment, which nobody does at 16:00 while chasing a hypothesis.

Where it goes wrong

Secrets in cells and in output. Both. A pasted connection string in a cell, and a token printed by a debugging line that got saved into the output. Rotating is the only remedy, and it only happens if somebody notices.

Data in the repository. A notebook that queried a customer table and displayed a dataframe has that dataframe in the file. This is the same problem as production data in staging, with the added property that git history keeps it forever after someone strips it.

Unscanned dependencies. In-cell installs, and often no requirements file at all. Your SCA coverage report says this repository is clean because there is nothing in it that a scanner recognises as a manifest.

Untrusted notebooks execute on open. A notebook from a colleague, a public repository or a Kaggle download can contain code that runs when you execute the first cell, and people run the first cell. Treat a .ipynb from outside with the same care as a script from outside, which is more care than most people apply.

The promotion path is invisible. A notebook becomes production not by being deployed but by being scheduled, which usually needs no review at all. That is the moment it should have entered your normal controls, and there is no gate at that moment.

What to actually do

Strip outputs on commit. One hook, and it removes the data-in-repository problem entirely.

pip install nbstripout
nbstripout --install          # installs a git filter for this repo

This is the highest-value single change here. It costs one command and it is invisible afterwards.

Scan notebooks for secrets, not just source files. Most secret scanners read .ipynb as text, so they will catch a pasted key if the file is in scope. Check that it is: some configurations exclude unfamiliar extensions by default, and a scanner that skips the file type reports clean.

Give them a real way to get credentials. This is the fix for inline secrets, and it is the only one that works, because the current behaviour exists for a reason. An environment variable injected by the notebook server, or a helper that fetches from your secrets manager in one line:

from company.creds import get   # reads from the secrets manager
conn = psycopg2.connect(get("analytics-db-readonly"))

If the safe path is longer than pasting the password, people will paste the password. Make it shorter.

Require a requirements file, and ban in-cell installs in anything scheduled. Exploratory notebooks can do whatever they like. The moment one is scheduled, its dependencies must be declared somewhere your scanning can see.

Put a gate at the promotion moment. Scheduling a notebook should require the same review as deploying a service: a look at what it accesses, what credentials it uses, and where its output goes. This is the single structural change that matters most, because it is the point where scratch space becomes infrastructure and currently nothing happens.

Scope the credentials. A notebook doing analysis needs read-only access to specific tables, not the application's database user. Most notebook credential incidents are bad because the credential was over-scoped, not because it leaked.

Do not ban notebooks

It will not work and it should not. Notebooks are the right tool for exploratory work, and a policy against them produces the same work happening in an untracked place, which is worse.

The productive framing is that a notebook has two lifecycle stages with different rules. Exploratory: minimal friction, scoped credentials, outputs stripped on commit. Scheduled or shared: treated as production code, with dependencies declared, secrets from the manager, and review.

The mistake is applying production rules to exploration, which people route around, or exploration rules to production, which is where you are now.

The concession

Stripping outputs has a genuine cost: a notebook's outputs are part of its value as a document. A committed notebook with charts and results is readable by someone who cannot run it, and that is often the point of writing it.

The workable compromise is to strip by default and make an explicit exception for notebooks intended as reports, with a review of what is in the outputs before that exception is granted. What does not work is deciding case by case at commit time, because that decision gets made in a hurry by the person least likely to notice a token in cell fourteen.

The implication

Notebooks are where an organisation's most sensitive data meets its least governed code, and the format actively hides both facts: the state is invisible and the data is inside the file.

Two changes get most of the value. Strip outputs automatically, so the data stops accumulating in git. Put a review at the moment a notebook gets scheduled, because that is when it stopped being an experiment and nobody currently notices.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.