DEV Community

Cover image for Agentic Supply Chain Vulnerabilities: Your Agent Is Only as Secure as Its Weakest Plugin (ASI04)
Maish Saidel-Keesing for AWS

Posted on • Originally published at technodrone.cloud

Agentic Supply Chain Vulnerabilities: Your Agent Is Only as Secure as Its Weakest Plugin (ASI04)

This is post #4 of the OWASP Agentic AI Top 10: What Builders on AWS Need to Know series.

You install an MCP server from npm. It says "postmark-mcp" on the label. Looks legitimate. Has a decent README. Your agent connects to it and starts routing emails through it.

I've built MCP integrations myself (IFTTT MCP Proxy). I know first-hand how easy it is to trust a tool descriptor without verifying what's behind it.

Except it's not the real Postmark MCP server. It's a lookalike that silently BCC's every email to an attacker. And because your agent trusted the package registry, it never questioned it.

This actually happened. It was the first documented malicious MCP server found in the wild. And it won't be the last.

Welcome to Agentic Supply Chain.

What Are Agentic Supply Chain Vulnerabilities?

ASI04 covers threats that arise when agents, tools, and the artifacts they depend on are provided by third parties who may be malicious, compromised, or tampered with in transit.

If you've dealt with traditional software supply chain attacks (think SolarWinds, Log4j, the npm event-stream incident), this sounds familiar. But agentic systems make it worse. Much worse.

Here's why. Traditional supply chain attacks target static dependencies - packages you install at build time. Agentic supply chains are live. Your agent discovers and connects to tools at runtime. It loads MCP servers dynamically. It registers with agent-to-agent protocols. It pulls prompt templates from external sources.

The attack surface isn't just your package.json. It's every tool, plugin, registry, MCP server, and peer agent your agent connects to during execution. And unlike your build pipeline, there's no lockfile for runtime tool discovery.

The MCP Supply Chain Crisis of 2026

Let me be blunt about the current state of affairs.

Between January and February 2026, security researchers filed roughly 30 CVEs against MCP servers, clients, and infrastructure. The root causes weren't exotic zero-days. They were missing input validation, absent authentication, and blind trust in tool descriptions.

Then in April 2026, OX Security disclosed what they called "the mother of all AI supply chains" - a systemic vulnerability baked into official MCP SDKs across Python, TypeScript, Java, and Rust. It exposed over 150 million downloads and up to 200,000 vulnerable instances to remote code execution.

Four named CVEs in the MCP layer alone during 2025: CVE-2025-6514 (mcp-remote RCE), CVE-2025-49596 (MCP Inspector), CVE-2025-54136 (Cursor), CVE-2025-54994 (create-mcp-server-stdio).

This isn't theoretical anymore. It's a full-blown crisis.

How Agentic Supply Chain Attacks Work

OWASP documents six primary attack vectors. Here are the ones hitting production right now.

1. Malicious MCP Server Impersonation

The Postmark case I mentioned above. An attacker publishes a package with a name that looks like a legitimate MCP server. Your agent (or your developer) installs it. It works... and also exfiltrates your data.

Real incident: The first documented malicious MCP server on npm impersonated postmark-mcp and secretly BCC'd all emails to the attacker.

2. Tool Descriptor Poisoning

An attacker doesn't need to compromise the tool itself. They just need to compromise the description of the tool. A malicious tool hides instructions in its MCP metadata. When your agent reads the description to decide how to use the tool, it follows the hidden instructions.

Real incident: GitHub MCP vulnerability - a researcher showed that a malicious public tool could hide commands in its metadata, causing the assistant to exfiltrate private repo data without the user's knowledge.

3. Compromised Registries and Agent Cards

An attacker compromises an MCP registry server or agent-to-agent protocol registry. Now they can serve tampered manifests, plugins, or agent descriptors at scale. Every agent that trusts that registry gets poisoned.

Real incident: Amazon Q Developer flaw - malicious repos could launch rogue MCP servers that stole AWS credentials automatically when the repository was cloned. Zero interaction required.

4. Poisoned Prompt Templates and Knowledge Plugins

An agent pulls prompt templates or RAG plugins from external sources. Those sources get compromised. Now the agent's behavior changes without any code changes in your system.

This is the slow-burn version. No alarms. No dependency updates. Just gradually poisoned context from a source you trusted.

Why Traditional Supply Chain Defenses Fall Short

You already have npm audit, Dependabot, SAST scanners. Great. But agentic supply chains break your existing tools in three ways:

Runtime discovery bypasses lockfiles. When your agent discovers tools via MCP at runtime, there's no lockfile to pin versions against. The tool that was safe yesterday might be compromised today.

Tool descriptions are attack vectors. Your dependency scanners check code for vulnerabilities. They don't check tool metadata for hidden prompt injection payloads.

Trust is transitive and unbounded. When your agent connects to a peer agent via Agent2Agent protocol, and that peer connects to another, your trust chain extends through systems you've never audited.

Mitigating Supply Chain Risks on AWS

Here's how to harden your agentic supply chain using AWS services. The theme is: don't trust anything by default, verify everything, and contain the blast radius.

1. CodeArtifact: Your Private Package Registry

AWS CodeArtifact gives you a private registry that sits between your agents and public registries (npm, PyPI). It acts as a gateway with approval controls.

import boto3

codeartifact = boto3.client("codeartifact")
DOMAIN = "my-company"

# 1. Ingest repo. The ONLY repo allowed to talk to public npm.
#    Agents never point at this one.
codeartifact.create_repository(
    domain=DOMAIN,
    repository="npm-ingest",
    description="Staging. Pulls from public npm for review.",
)
codeartifact.associate_external_connection(
    domain=DOMAIN,
    repository="npm-ingest",
    externalConnection="public:npmjs",
)

# 2. Agent repo. No external connection, no upstream to the internet.
#    Nothing lands here unless we put it here.
codeartifact.create_repository(
    domain=DOMAIN,
    repository="agent-approved-tools",
    description="Reviewed packages only. Agents pull from here.",
)

# 3. Approval IS the copy step. A human or a pipeline runs this after
#    review. The agent cannot. And you promote the whole dependency
#    tree, not just the top-level name.
def approve_package(name, version, fmt="npm", namespace=None):
    kwargs = dict(
        domain=DOMAIN,
        sourceRepository="npm-ingest",
        destinationRepository="agent-approved-tools",
        format=fmt,
        package=name,
        versions=[version],
    )
    if namespace:  # e.g. "modelcontextprotocol" for @modelcontextprotocol/...
        kwargs["namespace"] = namespace
    codeartifact.copy_package_versions(**kwargs)
Enter fullscreen mode Exit fullscreen mode

Then point the agent at the approved repo, and only that repo:

aws codeartifact login --tool npm --domain my-company --repository agent-approved-tools

Now you have a gate, a real one. A package does not reach an agent until someone copied it across on purpose. Good. But do not oversell it to yourself, because there are three things this setup still will not do for you.

It is not a firewall. That login command sets a preference, not a prison. It edits a config file. If the agent's box can still route to the open internet, it can ignore every word of the above and pull straight from registry.npmjs.org. The egress lockdown is not the optional bonus round. It is the whole point. No egress control, no gateway. You just have a nicely organized repo the agent is free to walk past.

The gate is only as good as the review. copy_package_versions is where a human is supposed to think. If a tired someone rubber-stamps whatever showed up in the ingest repo, congratulations, you have built a very well-architected way to approve malware. The tooling does not do the vetting. You do.

Dependencies are the real bill. Copying some-tool copies some-tool. It does not copy the forty packages it drags in behind it. A hard gate means you promote the entire tree, and you vet the entire tree, and yes, that is more work than letting a pull-through cache grab it all on demand. That friction is exactly why so many people quietly leave the cache wide open. Now you know the trade you are making.

2. Amazon Inspector: Continuous Vulnerability Scanning

Amazon Inspector continuously scans your Lambda functions and container images for known vulnerabilities. For agentic supply chains, this means:

  • Every Lambda function that backs an action group gets scanned
  • Every container image that runs an MCP server gets scanned
  • Vulnerabilities are flagged before they reach production
inspector = boto3.client("inspector2")

# Enable scanning for Lambda functions (agent tool backends)
inspector.enable(
    resourceTypes=["AWS_LAMBDA_FUNCTION", "AWS_ECR_CONTAINER_IMAGE"],
    accountIds=["123456789012"]
)

# Set up auto-remediation for critical findings
# EventBridge rule: Inspector finding → SNS → team notification
Enter fullscreen mode Exit fullscreen mode

3. AWS PrivateLink: Private Tool Connectivity

When your agent connects to internal tools or services, it should never traverse the public internet. PrivateLink keeps traffic on the AWS network.

This matters because MCP servers often expose HTTP endpoints. If those endpoints are public, an attacker can MITM the connection or redirect to a lookalike server. PrivateLink eliminates that entire class of attack.

ec2 = boto3.client("ec2")

# Create a VPC endpoint for your internal MCP server
endpoint = ec2.create_vpc_endpoint(
    VpcId="vpc-abc123",
    ServiceName="com.amazonaws.vpce.us-east-1.vpce-svc-mcp-internal",
    VpcEndpointType="Interface",
    SubnetIds=["subnet-123"],
    SecurityGroupIds=["sg-agent-tools"],
    PrivateDnsEnabled=True
)

# Agent's MCP client connects to the private DNS name
# Traffic never leaves the AWS network
Enter fullscreen mode Exit fullscreen mode

4. Lambda Layers: Controlled, Versioned Dependencies

Instead of letting each Lambda function manage its own dependencies (where drift happens), use Lambda layers as a controlled distribution mechanism:

  • One layer per dependency set, version-pinned
  • Layers are immutable once published
  • All agent-backing Lambdas reference the same approved layer version
  • Updating a layer requires a new version (auditible, rollback-able)
# SAM template: agent Lambda with pinned layer
AgentToolFunction:
  Type: AWS::Serverless::Function
  Properties:
    Handler: index.handler
    Runtime: python3.12
    Layers:
      # Pinned to a specific, approved layer version
      - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:agent-deps-approved:7"
    # No internet access - can't pull packages at runtime
    VpcConfig:
      SecurityGroupIds: [!Ref AgentToolSG]
      SubnetIds: [!Ref PrivateSubnet]
Enter fullscreen mode Exit fullscreen mode

The VPC config with no internet access is critical. Even if malicious code gets in, it can't phone home.

5. AWS Signer: Code Signing for Trust

AWS Signer lets you sign your Lambda deployment packages and layer versions. Configure Lambda to reject unsigned or untrusted code:

lambda_client = boto3.client("lambda")

# Create a code signing config
config = lambda_client.create_code_signing_config(
    AllowedPublishers={
        "SigningProfileVersionArns": [
            "arn:aws:signer:us-east-1:123456789012:signing-profiles/AgentToolSigner"
        ]
    },
    CodeSigningPolicies={
        "UntrustedArtifactOnDeployment": "Enforce"  # REJECT unsigned code
    }
)

# Attach to your agent's Lambda functions
lambda_client.put_function_code_signing_config(
    CodeSigningConfigArn=config["CodeSigningConfig"]["CodeSigningConfigArn"],
    FunctionName="agent-order-lookup"
)
Enter fullscreen mode Exit fullscreen mode

You flipped it to Enforce. You have a real gate now. Lambda will refuse anything not signed by a publisher you trust. Good. But be precise about what that signature is actually swearing to, because it is easy to hear more than it says.

A signature proves origin, not innocence. All it says is "this is the exact artifact your pipeline built and signed." It does not say the artifact is safe. If a poisoned dependency slipped into your build, you will sign it, Lambda will happily accept it, and everyone will feel great about the malware that just shipped with a valid signature. The signing is only as honest as what you handed the signer. Sound familiar? It should.

It checks at the door, not in the room. Verification happens at deploy time. Once the function is live it runs on every invocation and nothing re-checks it. You have proven what you
shipped. You have not proven how it behaves at 3am.

And the part everyone skips: who can turn it off? This whole control exists to stop an attacker who already has credentials. So ask the obvious question. What stops that same attacker from detaching the code signing config, or flipping Enforce back to Warn, and then deploying whatever they please? Nothing in this snippet does. A lock is only a lock if the intruder cannot reach the off switch. Guard the Lambda config permissions, back them with an SCP, or you have built a very official looking door that opens from the inside.

6. ECR Image Scanning: Container-Level Verification

If your MCP servers run as containers, ECR with scan-on-push catches vulnerabilities before they reach your agents:

ecr = boto3.client("ecr")

# Enable scan-on-push for MCP server images
ecr.put_image_scanning_configuration(
    repositoryName="mcp-servers",
    imageScanningConfiguration={
        "scanOnPush": True
    }
)

# Block deployment of images with CRITICAL findings
# EventBridge rule: ECR scan complete → check findings → block if critical
Enter fullscreen mode Exit fullscreen mode

The Architecture: Supply Chain Kill Chain

Here's the full defensive stack:

  1. Package ingestion → CodeArtifact (private registry, approval gates)
  2. Code signing → AWS Signer (reject unsigned deployments)
  3. Vulnerability scanning → Inspector + ECR scan-on-push
  4. Runtime isolation → Lambda in VPC, no internet, PrivateLink for tool connectivity
  5. Dependency control → Lambda layers (immutable, versioned, auditable)
  6. Continuous monitoring → Inspector continuous scanning + CloudTrail for deployment changes

If an attacker compromises a public MCP package, they hit the CodeArtifact gate. If they somehow get past that, Signer rejects the unsigned code. If they forge a signature, Inspector catches the vulnerability. If they somehow deploy clean-looking code, the Lambda has no internet access to exfiltrate data.

That's defense in depth applied to supply chain.

Key Takeaway

Your agent's supply chain is not your package.json. It's every tool, MCP server, registry, peer agent, and prompt template your agent touches at runtime. Treat each one as untrusted until verified, signed, scanned, and isolated.

Up Next

Post 5: Unexpected Code Execution (ASI05) - When "let the agent write code" meets "and then run it unsandboxed." How Bedrock AgentCore, Lambda, and Fargate keep agent-generated code from owning your infrastructure.

I would be very interested to hear your thoughts or comments, so please feel free to ping me on LinkedIn or Twitter, or drop them below. If you're running MCP servers in production, I'd genuinely love to hear how you're handling trust.

Onward!!

Top comments (0)