← Back to Blog

Eight data-flow security habits from running real pipelines (Part 5 of 5)

Eight data-flow security habits from running real pipelines (Part 5 of 5)

Most AI security writing worries about what a model says in a chat box. The pipelines I actually run do not have a chat box. They fetch pages an attacker can edit, summarize documents I did not write, and hand structured output to code that spends money or ships data to a vendor. The risk was never the model being rude. The risk is data flowing across a boundary it should not cross, quietly, while nobody is watching.

The first four parts of this series drew the boundaries: the three trust boundaries, indirect prompt injection, coding agents as remote code execution, and the action boundary. This part is the field notes: eight habits, each one paid for by an outage or a near miss, each generalized so you can adopt it this week on whatever you are building, a RAG app, a browsing agent, or a nightly pipeline nobody looks at until it breaks.

These habits map onto the OWASP Top 10 for LLM Applications (2025), the OWASP Agentic AI Threats and Mitigations guidance, MITRE ATLAS, and the NIST AI Risk Management Framework. I will name the relevant items as I go, but the point is the habit, not the citation.

1. Run an SSRF check on every fetch of a non-constant URL

The moment your system follows a URL it did not hard-code, you have a Server-Side Request Forgery problem. An agent that browses a page, a summarizer that follows a link in a document, a webhook that fetches an "avatar URL" from a payload: all of these let someone else choose where your server points.

The nasty version is not "fetch a huge file." It is fetch http://169.254.169.254/, the cloud metadata endpoint, and read back the credentials your instance runs under. Or fetch http://localhost:8080/admin and reach a service that trusted you because you were on the same host.

The habit: before any fetch of a URL you did not author, resolve it and refuse internal targets.

def safe_fetch(url):
    host = resolve(url)                 # resolve DNS first
    if is_private(host)     \
       or is_loopback(host) \
       or is_link_local(host)           # 169.254.0.0/16, incl metadata
       or is_unique_local(host):        # fc00::/7
        raise Blocked(url)
    return fetch(url, allow_redirects=False)

Block redirects, or re-check every hop, because a public URL can 302 you straight into the private range. This is Unbounded Consumption and tool-misuse territory, and it is the cheapest control with the highest payoff.

2. Put auth on every endpoint, including the "internal" ones

I used to believe "internal" was a trust boundary. On a shared network, a home lab, or any cloud VPC that a misconfigured security group exposes, it is not. If a route mutates state or reveals data, it needs a token. All of them. The status page. The ingest hook. The kill switch especially.

The kill switch is the one that hurts. A shutdown endpoint with no auth is a denial-of-service button for anyone who can reach it, and that set is larger than you think. Treat every route as public and design accordingly.

  • Any state-changing route: token required.
  • Any data-exposing route: token required.
  • "It is only on localhost" is not authentication.
  • Health checks can be open. Nothing that reads or writes real data can.

3. Running equals committed

If what is live differs from what is in version control, you have lost the ability to audit, reproduce, or detect tampering. You cannot answer "what code is running right now?" and that question is the whole game during an incident.

The way this bites you is subtle. A CLI deploy from your laptop bundles whatever is in the working directory, including untracked files you forgot about, a debug patch, a hard-coded key, a half-finished experiment. Now production contains code that exists nowhere in git. When something goes wrong, the diff you are reading is fiction.

The habit: deploy only from version control, and make the build assert it.

running_sha = os.environ["BUILD_COMMIT"]
head_sha    = git("rev-parse", "HEAD")
assert running_sha == head_sha, "refusing to run: build != HEAD"

If a poisoned instruction ever does slip into a coding agent's output, this is the tripwire that makes the tampering visible instead of invisible. It is Supply Chain and Improper Output Handling insurance, and it costs one assertion.

4. One writer per dataset, plus a pre-publish sanity guard

A scraper I run once overwrote most of a weekend of good data with an empty file, because a source went dark mid-run and the pipeline cheerfully published the zero-row result on top of the real one. The bug was not the outage. The bug was that a half-finished run was allowed to clobber a finished one.

Two rules fix an entire class of these:

  • Exactly one authorized writer per dataset. Two processes writing the same store is a race and an audit hole. If a second thing needs to write, it proposes and the one writer commits.
  • A pre-publish sanity guard. Before the swap, refuse to publish when the new data is empty or wildly off from the last known good version.
def publish(new, current):
    if new.rows == 0:
        abort("zero rows, keeping current")
    if new.rows < current.rows * 0.5:      # tune per dataset
        abort("row count collapsed, hold for review")
    swap(new)

A poisoned source, a network blip, and a crashed run all present the same way at the output: garbage or emptiness. One guard catches all three.

5. Know exactly what data leaves the box, and to whom

Every call to a third-party model API is data egress to a vendor, and that fact gets ignored constantly. The text you paste into a hosted model, the support ticket, the internal document, the user's message, has left your control and entered someone else's logs and retention policy.

The habit: keep an explicit allowlist of what is permitted to leave, and keep the sensitive stuff on local models.

  • Classify data before it can reach an outbound API. If it is not on the allowlist, it does not go.
  • Route sensitive categories (anything regulated, anything a user would be upset to see in a vendor's training set) to a local or self-hosted model.
  • Log every egress: what class of data, to which vendor, from which code path. You want to answer "did PII ever leave?" with a query, not a guess.

This is the habit that makes a privacy review tractable instead of theatrical.

6. A leaked URL or key is a financial denial-of-service, not only a data risk

We frame leaked credentials as a confidentiality problem. On metered AI infrastructure they are also a spending problem. A public endpoint with no rate limit, or an API key that ends up in a client bundle or a git history, can burn a month of paid quota in an afternoon. The attacker does not need your data. They just need your billing to be someone else's free compute.

The habit is a small kit you set up once:

  • Per-key rate limits, enforced server-side, not client-side.
  • Budget alerts at thresholds you will actually notice.
  • Key rotation on a schedule, and a rehearsed revocation drill so you can kill a leaked key in minutes, not hours.
  • Never ship a provider key to the browser. Proxy through your own backend.

Rotation you have never practised is not a control. Do the drill once, before you need it.

7. Verify TLS always, and prefer official sources over scraped mirrors

Somewhere in every codebase there is a tempting verify=False that makes a stubborn request finally work. It also makes you trivially man-in-the-middle-able, which means the data feeding your model or pipeline can be swapped in transit. Never disable certificate verification. If a cert is broken, fix the cert or the trust store, do not blind the client.

Related habit: prefer the official, signed, or bulk source over a convenient scraped mirror. A mirror is an extra party who can alter the data and an extra point of failure. Bulk downloads and signed releases give you integrity you can check. Scraped copies give you neither, plus a Terms of Service headache, which is the last habit.

8. Honor Terms of Service and robots.txt, and respect the CFAA line

Some official data sources gate access behind an agreement you accept programmatically. Honoring that acceptance, and logging when and which version you accepted, is part of staying lawful and part of being auditable later. Read robots.txt and honor it.

The bright line: do not bypass authentication or paywalls to get data. In the US that raises Computer Fraud and Abuse Act exposure, and it is a genuine legal risk, not a theoretical one. The public-scraping case law, the hiQ v LinkedIn line, is a note of caution about publicly available data, not a green light to route around a login. If the data sits behind auth, you need permission, not a clever client.

  • Accept and log ToS acceptance where required.
  • Read and honor robots.txt.
  • Never defeat auth or paywalls.
  • Treat "public" narrowly: reachable without credentials, and permitted by the terms.

The through-line

Every one of these is the same move in a different costume: name a boundary, then refuse to let data cross it silently. A URL becomes a fetch target only after a check. Data becomes a vendor's problem only if it is on the allowlist. A new dataset becomes live only after the guard. Output becomes the running system only if it equals HEAD. Chatbot security asks whether the model behaves. Pipeline security asks who is allowed to touch what, and proves it in code you can read at 3am.

Pick two of these you do not do yet and add them this week. They are all cheap. I paid for them the expensive way so you can copy them the cheap way.

Previous: Never let a model pull the trigger: the action boundary for autonomous AI

Related reading

This post is informational, not security-consulting advice. Framework references are nominative fair use; no affiliation is implied.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026