|
|
| Severity |
Critical |
| Category |
Exposed secret / credential leakage |
| Component |
scripts/ |
| Affected |
@percolatorct/sdk v4.3.0 — verified at main = c976188 |
| Introduced |
commit 4852fbc, 12 April 2026 (~4 months of public exposure) |
| Status |
Confirmed by file inspection and git-history analysis on a full clone |
| CWE |
CWE-798 (Hard-coded Credentials), CWE-540 (Sensitive Information in Source Code), CWE-538 (Insertion of Sensitive Information into Externally-Accessible File) |
| Action required |
Rotate the key today. This is not precautionary. |
1. Summary
A live Helius mainnet RPC API key is embedded as a plaintext string literal in nine committed script files, and has been publicly readable in this repository for approximately four months. Anyone who has cloned, forked, or browsed the repository at any point since 12 April 2026 holds working credentials for a paid mainnet RPC endpoint.
The exposed value is:
ecfc91c7-b704-4c37-b10e-a277392830aa
Separately, fifteen scripts hard-code absolute filesystem paths belonging to a specific operator machine, eleven of which point at a mainnet deploy-authority keypair. No private key material is committed — only its location — but the disclosure is useful to an attacker and the paths also make the scripts unrunnable for anyone else.
This finding is independent of every other issue in the accompanying audit and should be actioned before any of them.
2. Affected locations
2.1 The API key — 9 occurrences in 9 files
| File |
Line |
scripts/close-market-v3.ts |
28 |
scripts/diag-slab.ts |
6 |
scripts/dump-config-bytes.ts |
8 |
scripts/dump-oracle-bytes.ts |
11 |
scripts/finalize-market.ts |
25 |
scripts/inspect-slab.ts |
7 |
scripts/inspect-slab2.ts |
5 |
scripts/probe-close-account.ts |
15 |
scripts/probe-resolve-live.ts |
12 |
Representative occurrence — scripts/finalize-market.ts:25:
const HELIUS_RPC = 'https://mainnet.helius-rpc.com/?api-key=ecfc91c7-b704-4c37-b10e-a277392830aa';
const conn = new Connection(HELIUS_RPC, 'confirmed');
2.2 Operator path disclosure — 15 occurrences in 15 files
Eleven references to the mainnet deploy-authority keypair, e.g. scripts/probe-close-account.ts:17:
const admin = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync('/Users/khubair/.percolator-mainnet/keys/deploy-authority.json', 'utf8')))
);
Also present in close-market-v3.ts:31, finalize-market.ts:28, probe-closeslab.ts:13, probe-closeslab2.ts:13, probe-resolve-live.ts:14, probe-resolve2.ts:11, and via os.homedir() in bootstrap-hyperp-oracle.mjs:18, fund-keeper.mjs:11, jup-swap.mjs:10, probe-crank.mjs:18.
Seven .mjs scripts additionally import the SDK through an absolute path, e.g. scripts/check-slab.mjs:2:
import { parseConfig, parseHeader } from '/Users/khubair/percolator-sdk/dist/index.js';
These fail with ERR_MODULE_NOT_FOUND on any machine but the author's.
3. Root cause
Three factors combined; each is individually addressable.
3.1 Configuration was written as source, not read from the environment. The endpoint URL, including its credential, is a const in application code rather than a value read at runtime. There is no code path by which a different key could be supplied.
3.2 The existing secret control does not match this shape of secret. .gitignore already carries a deliberate credentials block:
# Private keys, certificates, and credentials
*.pem
*.key
*.p12
*.pfx
id_rsa*
id_ed25519*
credentials.json
keypair.json
*-keypair.json
Every pattern matches a file whose whole purpose is to hold a secret. None matches a credential written inline inside a .ts file that also contains ordinary application logic. The intent to protect secrets exists; the mechanism has a blind spot precisely where this key sits.
3.3 No automated detection. .github/workflows/ contains CI, release, parity-gate, and devnet-smoke workflows, but no secret scanner. Nothing in the pipeline would have failed on the introducing commit, and nothing has flagged it in the four months since.
3.4 Why npm packaging did not contain the damage. package.json declares:
so scripts/ is excluded from the published npm tarball. This is worth stating explicitly because it is a real mitigation for one distribution channel — and equally worth stating that it is irrelevant to this finding. The repository itself is public; the exposure is the repository, not the package.
4. Proof of concept
Everything below is read-only. No request was made to the Helius endpoint with the exposed key, and no attempt was made to exercise it.
4.1 Enumerate the credential at HEAD
git clone https://github.com/dcccrypto/percolator-sdk
cd percolator-sdk
git rev-parse --short HEAD
grep -rno "ecfc91c7-b704-4c37-b10e-a277392830aa" scripts/ | sed 's/:ecfc91c7.*//'
Actual output:
c976188
scripts/close-market-v3.ts:28
scripts/diag-slab.ts:6
scripts/dump-config-bytes.ts:8
scripts/dump-oracle-bytes.ts:11
scripts/finalize-market.ts:25
scripts/inspect-slab.ts:7
scripts/inspect-slab2.ts:5
scripts/probe-close-account.ts:15
scripts/probe-resolve-live.ts:12
4.2 Establish the exposure window — the material result
echo "commits touching the key:"
git log --all --oneline -S 'ecfc91c7-b704-4c37-b10e-a277392830aa' | wc -l
echo "first introduced:"
git log --all --format="%h %ad %an %s" --date=short \
-S 'ecfc91c7-b704-4c37-b10e-a277392830aa' --reverse | head -1
echo "files at HEAD:"
git grep -l 'ecfc91c7' HEAD -- scripts/
Actual output:
commits touching the key:
2
first introduced:
4852fbc 2026-04-12 dcccrypto chore: remove keeper fund, update v12.15 layout constants
files at HEAD:
scripts/close-market-v3.ts
scripts/diag-slab.ts
scripts/dump-config-bytes.ts
scripts/dump-oracle-bytes.ts
scripts/finalize-market.ts
scripts/inspect-slab.ts
scripts/inspect-slab2.ts
scripts/probe-close-account.ts
scripts/probe-resolve-live.ts
This is the finding that determines the response. The credential entered history on 12 April 2026 and has been continuously public since. Two consequences follow with no room for interpretation:
- Rotation is mandatory, not precautionary. A credential public for four months must be treated as compromised. The absence of an anomalous billing spike is not evidence of non-use — low-rate abuse is indistinguishable from normal traffic on a shared quota.
- Deleting the string at
HEAD is insufficient. Because the value lives in history, it remains retrievable through git log -p, from every existing clone, and from every fork — including forks created after 4852fbc. History rewriting is required, and even then third-party copies persist. This is precisely why step 1 of the remediation is rotation and not deletion.
4.3 Confirm .gitignore does not cover the file
git check-ignore -v scripts/finalize-market.ts || echo " scripts/finalize-market.ts: NOT ignored"
Actual output:
scripts/finalize-market.ts: NOT ignored
4.4 Quantify the path disclosure
echo "key occurrences: $(grep -ro 'ecfc91c7-b704-4c37-b10e-a277392830aa' scripts/ | wc -l) in $(grep -rl 'ecfc91c7' scripts/ | wc -l) files"
echo "absolute /Users/khubair paths: $(grep -ro '/Users/khubair' scripts/ | wc -l) in $(grep -rl '/Users/khubair' scripts/ | wc -l) files"
echo "deploy-authority.json refs: $(grep -ro 'deploy-authority.json' scripts/ | wc -l)"
Actual output:
key occurrences: 9 in 9 files
absolute /Users/khubair paths: 15 in 15 files
deploy-authority.json refs: 11
5. Impact
5.1 Quota exhaustion and denial of service. Helius bills by request volume. A third party using this key consumes the owner's paid quota. Sustained use exhausts the plan and degrades or disables every service depending on that endpoint. For Percolator specifically this includes keepers and crankers, whose failure has direct protocol consequences: missed liquidations, stalled funding accrual, and unadvanced bankruptcy settlement.
5.2 Operational traffic observation. Whoever controls or observes traffic on that endpoint sees the query pattern of the operator's infrastructure — which markets are polled, at what cadence, and immediately before which transactions. On a perpetuals protocol this is exploitable information independent of any credential misuse.
5.3 Reconnaissance value of the path disclosure. /Users/khubair/.percolator-mainnet/keys/deploy-authority.json confirms that a mainnet upgrade-authority keypair exists, on a macOS machine, at a known path, under a known username. That is materially useful to an attacker who obtains any other foothold on that machine, and it narrows a targeted phishing or supply-chain attempt.
5.4 Scripts are unusable by anyone else. The absolute import paths mean seven .mjs scripts fail immediately for any other contributor. This is a maintainability defect rather than a security one, but it shares a root cause and should be fixed in the same change.
6. Remediation
Perform these in order. Step 1 is time-critical and must not wait for the others.
Step 1 — Rotate the credential now
Revoke ecfc91c7-b704-4c37-b10e-a277392830aa in the Helius dashboard and issue a replacement. Do not reuse the value anywhere. Treat it as fully compromised regardless of observed usage.
If your Helius plan supports it, bind the replacement key to the IP ranges your infrastructure actually uses. This limits the blast radius of any future exposure without changing your workflow.
Step 2 — Move configuration to the environment
Apply to all nine files carrying the key:
- const HELIUS_RPC = 'https://mainnet.helius-rpc.com/?api-key=ecfc91c7-b704-4c37-b10e-a277392830aa';
- const conn = new Connection(HELIUS_RPC, 'confirmed');
+ const HELIUS_RPC = process.env.PERCOLATOR_RPC_URL;
+ if (!HELIUS_RPC) {
+ throw new Error(
+ 'PERCOLATOR_RPC_URL is not set. Export it before running this script, e.g.\n' +
+ ' export PERCOLATOR_RPC_URL="https://mainnet.helius-rpc.com/?api-key=<your-key>"',
+ );
+ }
+ const conn = new Connection(HELIUS_RPC, 'confirmed');
Apply the same treatment to the keypair paths:
- const admin = Keypair.fromSecretKey(
- Buffer.from(JSON.parse(fs.readFileSync('/Users/khubair/.percolator-mainnet/keys/deploy-authority.json', 'utf8')))
- );
+ const ADMIN_KEYPAIR = process.env.PERCOLATOR_ADMIN_KEYPAIR;
+ if (!ADMIN_KEYPAIR) {
+ throw new Error('PERCOLATOR_ADMIN_KEYPAIR is not set (path to the deploy-authority keypair JSON).');
+ }
+ const admin = Keypair.fromSecretKey(
+ Buffer.from(JSON.parse(fs.readFileSync(ADMIN_KEYPAIR, 'utf8')))
+ );
And replace the absolute SDK imports with relative ones:
- import { parseConfig, parseHeader } from '/Users/khubair/percolator-sdk/dist/index.js';
+ import { parseConfig, parseHeader } from '../dist/index.js';
No new dependencies are needed: dotenv is already a devDependency, and .env / .env.* are already gitignored.
A .env.example committed alongside documents the contract without carrying any secret:
# .env.example — copy to .env and fill in. .env is gitignored.
PERCOLATOR_RPC_URL=https://mainnet.helius-rpc.com/?api-key=YOUR_KEY_HERE
PERCOLATOR_ADMIN_KEYPAIR=/absolute/path/to/deploy-authority.json
Step 3 — Purge the value from git history
# Requires git-filter-repo (pip install git-filter-repo)
git filter-repo --replace-text <(echo 'ecfc91c7-b704-4c37-b10e-a277392830aa==>REDACTED')
git push --force-with-lease --all
git push --force-with-lease --tags
Then notify anyone holding a fork or clone. Their copies retain the original value regardless of upstream rewriting; only rotation (step 1) actually neutralises it.
Step 4 — Add automated prevention
Extend .github/workflows/ci.yml so this class of leak fails the build rather than depending on review:
- name: Secret scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Optionally add a pre-commit hook for local feedback:
# .git/hooks/pre-commit
if git diff --cached | grep -qE 'api[-_]?key=[0-9a-f]{8}-'; then
echo "error: possible API key in staged changes"; exit 1
fi
Step 5 — Broaden the .gitignore comment
The existing block is correct but its heading implies wider coverage than it provides. A one-line clarification prevents the next person from assuming inline secrets are handled:
# Private keys, certificates, and credentials
+ # NOTE: these patterns match secret-bearing FILES only. Credentials written
+ # inline inside source files are not covered — use environment variables.
*.pem
7. Verification checklist for the fix
# 1. No occurrence of the old key anywhere in the tree or history.
grep -r "ecfc91c7" . && echo "STILL PRESENT" || echo "clean"
git log --all -S 'ecfc91c7-b704-4c37-b10e-a277392830aa' --oneline | wc -l # expect 0
# 2. No absolute operator paths remain.
grep -r "/Users/khubair" scripts/ && echo "STILL PRESENT" || echo "clean"
# 3. Scripts fail loudly without configuration rather than using a baked-in default.
unset PERCOLATOR_RPC_URL
npx tsx scripts/diag-slab.ts # expect: "PERCOLATOR_RPC_URL is not set."
# 4. Secret scanning is wired into CI.
grep -q gitleaks .github/workflows/ci.yml && echo "scanner present"
8. References
package.json — "files": ["dist"], which limits npm distribution only
.gitignore — the existing credentials block and its blind spot
.github/workflows/ — CI, release, parity-gate, devnet-smoke; no secret scanner
- Commit
4852fbc (2026-04-12) — introduces the credential
- CWE-798: Use of Hard-coded Credentials
- CWE-540: Inclusion of Sensitive Information in Source Code
- CWE-538: Insertion of Sensitive Information into Externally-Accessible File or Directory
scripts/@percolatorct/sdkv4.3.0 — verified atmain=c9761884852fbc, 12 April 2026 (~4 months of public exposure)1. Summary
A live Helius mainnet RPC API key is embedded as a plaintext string literal in nine committed script files, and has been publicly readable in this repository for approximately four months. Anyone who has cloned, forked, or browsed the repository at any point since 12 April 2026 holds working credentials for a paid mainnet RPC endpoint.
The exposed value is:
Separately, fifteen scripts hard-code absolute filesystem paths belonging to a specific operator machine, eleven of which point at a mainnet deploy-authority keypair. No private key material is committed — only its location — but the disclosure is useful to an attacker and the paths also make the scripts unrunnable for anyone else.
This finding is independent of every other issue in the accompanying audit and should be actioned before any of them.
2. Affected locations
2.1 The API key — 9 occurrences in 9 files
scripts/close-market-v3.tsscripts/diag-slab.tsscripts/dump-config-bytes.tsscripts/dump-oracle-bytes.tsscripts/finalize-market.tsscripts/inspect-slab.tsscripts/inspect-slab2.tsscripts/probe-close-account.tsscripts/probe-resolve-live.tsRepresentative occurrence —
scripts/finalize-market.ts:25:2.2 Operator path disclosure — 15 occurrences in 15 files
Eleven references to the mainnet deploy-authority keypair, e.g.
scripts/probe-close-account.ts:17:Also present in
close-market-v3.ts:31,finalize-market.ts:28,probe-closeslab.ts:13,probe-closeslab2.ts:13,probe-resolve-live.ts:14,probe-resolve2.ts:11, and viaos.homedir()inbootstrap-hyperp-oracle.mjs:18,fund-keeper.mjs:11,jup-swap.mjs:10,probe-crank.mjs:18.Seven
.mjsscripts additionally import the SDK through an absolute path, e.g.scripts/check-slab.mjs:2:These fail with
ERR_MODULE_NOT_FOUNDon any machine but the author's.3. Root cause
Three factors combined; each is individually addressable.
3.1 Configuration was written as source, not read from the environment. The endpoint URL, including its credential, is a
constin application code rather than a value read at runtime. There is no code path by which a different key could be supplied.3.2 The existing secret control does not match this shape of secret.
.gitignorealready carries a deliberate credentials block:Every pattern matches a file whose whole purpose is to hold a secret. None matches a credential written inline inside a
.tsfile that also contains ordinary application logic. The intent to protect secrets exists; the mechanism has a blind spot precisely where this key sits.3.3 No automated detection.
.github/workflows/contains CI, release, parity-gate, and devnet-smoke workflows, but no secret scanner. Nothing in the pipeline would have failed on the introducing commit, and nothing has flagged it in the four months since.3.4 Why npm packaging did not contain the damage.
package.jsondeclares:so
scripts/is excluded from the published npm tarball. This is worth stating explicitly because it is a real mitigation for one distribution channel — and equally worth stating that it is irrelevant to this finding. The repository itself is public; the exposure is the repository, not the package.4. Proof of concept
Everything below is read-only. No request was made to the Helius endpoint with the exposed key, and no attempt was made to exercise it.
4.1 Enumerate the credential at
HEADActual output:
4.2 Establish the exposure window — the material result
Actual output:
This is the finding that determines the response. The credential entered history on 12 April 2026 and has been continuously public since. Two consequences follow with no room for interpretation:
HEADis insufficient. Because the value lives in history, it remains retrievable throughgit log -p, from every existing clone, and from every fork — including forks created after4852fbc. History rewriting is required, and even then third-party copies persist. This is precisely why step 1 of the remediation is rotation and not deletion.4.3 Confirm
.gitignoredoes not cover the fileActual output:
4.4 Quantify the path disclosure
Actual output:
5. Impact
5.1 Quota exhaustion and denial of service. Helius bills by request volume. A third party using this key consumes the owner's paid quota. Sustained use exhausts the plan and degrades or disables every service depending on that endpoint. For Percolator specifically this includes keepers and crankers, whose failure has direct protocol consequences: missed liquidations, stalled funding accrual, and unadvanced bankruptcy settlement.
5.2 Operational traffic observation. Whoever controls or observes traffic on that endpoint sees the query pattern of the operator's infrastructure — which markets are polled, at what cadence, and immediately before which transactions. On a perpetuals protocol this is exploitable information independent of any credential misuse.
5.3 Reconnaissance value of the path disclosure.
/Users/khubair/.percolator-mainnet/keys/deploy-authority.jsonconfirms that a mainnet upgrade-authority keypair exists, on a macOS machine, at a known path, under a known username. That is materially useful to an attacker who obtains any other foothold on that machine, and it narrows a targeted phishing or supply-chain attempt.5.4 Scripts are unusable by anyone else. The absolute import paths mean seven
.mjsscripts fail immediately for any other contributor. This is a maintainability defect rather than a security one, but it shares a root cause and should be fixed in the same change.6. Remediation
Perform these in order. Step 1 is time-critical and must not wait for the others.
Step 1 — Rotate the credential now
Revoke
ecfc91c7-b704-4c37-b10e-a277392830aain the Helius dashboard and issue a replacement. Do not reuse the value anywhere. Treat it as fully compromised regardless of observed usage.If your Helius plan supports it, bind the replacement key to the IP ranges your infrastructure actually uses. This limits the blast radius of any future exposure without changing your workflow.
Step 2 — Move configuration to the environment
Apply to all nine files carrying the key:
Apply the same treatment to the keypair paths:
And replace the absolute SDK imports with relative ones:
No new dependencies are needed:
dotenvis already a devDependency, and.env/.env.*are already gitignored.A
.env.examplecommitted alongside documents the contract without carrying any secret:Step 3 — Purge the value from git history
Then notify anyone holding a fork or clone. Their copies retain the original value regardless of upstream rewriting; only rotation (step 1) actually neutralises it.
Step 4 — Add automated prevention
Extend
.github/workflows/ci.ymlso this class of leak fails the build rather than depending on review:Optionally add a pre-commit hook for local feedback:
Step 5 — Broaden the
.gitignorecommentThe existing block is correct but its heading implies wider coverage than it provides. A one-line clarification prevents the next person from assuming inline secrets are handled:
7. Verification checklist for the fix
8. References
package.json—"files": ["dist"], which limits npm distribution only.gitignore— the existing credentials block and its blind spot.github/workflows/— CI, release, parity-gate, devnet-smoke; no secret scanner4852fbc(2026-04-12) — introduces the credential