Skip to content

fix: security and robustness hardening across core libs - #48

Open
LautaroPetaccio wants to merge 2 commits into
mainfrom
fix/security-robustness-hardening
Open

fix: security and robustness hardening across core libs#48
LautaroPetaccio wants to merge 2 commits into
mainfrom
fix/security-robustness-hardening

Conversation

@LautaroPetaccio

Copy link
Copy Markdown
Contributor

Summary

Fixes from a codebase-wide review for bugs, security issues, and robustness. Every fix has test coverage; the full workspace build, test suite (935 tests, 0 failures), and lint all pass. Changesets are included (7 packages patch + @dcl/http-commons minor; decentraland-crypto-fetch auto-bumps as a @dcl/crypto dependent).

⚠️ Two consumer-visible decisions to review

  1. @dcl/content-validator — subgraph third-party ownership now fails closed. The subgraph access checker previously skipped third-party wearable/emote ownership (it silently discarded the third-party URN buckets), letting a profile/outfit reference an unowned third-party item and pass validation. It has no way to verify third-party ownership, so it now rejects those deployments. Deployments using the on-chain checker are unaffected. If any consumer relies on the subgraph checker for third-party content, this will start rejecting it.
  2. @dcl/crypto-middleware — metadata gap documented, not changed. The signed-fetch payload is lowercased by wire-protocol convention shared with all Decentraland signers, so authMetadata is authenticated case-insensitively only. Removing the lowercasing would break interop, so this is documented (JSDoc + existing README) rather than changed.

Changes by package

  • urn-resolverparseUrn/resolveUrlFromUrn return null instead of throwing on malformed input (bad %-escapes, non-URL input, and invalid/out-of-bounds LAND positions that threw via BigInt); entity resolver restricts baseUrl to http(s) and validates cid (blocks javascript:, open-redirect, path-traversal); case-insensitive network check; strict parcel coordinates.
  • content-validator — subgraph third-party fail-closed (above); safeParseUrn in common access checks; unknown-entity-type guard; inclusive ADR-74 boundary; strict scene pointer parsing.
  • http-commons — request body-size guard + PayloadTooLargeError (413); log stack on 500s (server-side only, not leaked); digest-based constant-time bearer comparison + case-insensitive scheme; strict pagination; error-class names; files/exports in package.json.
  • crypto — empty auth chain is now invalid; unknown auth-link types fail closed cleanly (no throw to caller).
  • hashing — dropped ES2023 toSorted (older-runtime compat); layout-option validation; realm-safe buffer check; documented keccak256Hash ordering sensitivity.
  • single-sign-on-clientgetIdentity validates identity shape and clears/returns null on malformed stored values instead of throwing.
  • crypto-middleware / crypto-fetch — documented the metadata case-insensitivity property in code.

Intentionally not changed

  • Open redirect to an arbitrary https host via baseUrl — arbitrary https gateways are that parameter's legitimate purpose; blocking hosts would need an allowlist. Only javascript:/malformed/traversal are blocked.
  • crypto EIP-1654 per-call construction (perf, off hot path) — caching mutable block state risks staleness.
  • content-validator outfit slot-count check and a .findSet perf tweak — low value / ambiguous.

Test plan

  • pnpm -r build
  • pnpm -r test — 935 tests pass
  • pnpm -r lint

LautaroPetaccio and others added 2 commits July 2, 2026 09:43
Addresses findings from a codebase-wide review. All fixes include test
coverage; full workspace build, test suite (935 tests), and lint pass.

- urn-resolver: parseUrn/resolveUrlFromUrn return null instead of throwing
  on malformed input (bad %-escapes, non-URL input, invalid/out-of-bounds
  LAND positions); entity resolver restricts baseUrl to http(s) and validates
  cid (blocks javascript:/open-redirect/path-traversal); case-insensitive
  network check; strict parcel coordinate parsing.
- content-validator: subgraph access checker now fails closed on third-party
  ownership it cannot verify (behavior change: rejects such deployments on the
  subgraph path; use the on-chain checker for third-party content); safeParseUrn
  in common access checks; guard unknown entity type; inclusive ADR-74 boundary;
  strict scene pointer parsing.
- http-commons: body-size guard with PayloadTooLargeError (413); log stack on
  500 (server-side only); digest-based constant-time bearer compare and
  case-insensitive scheme; strict pagination; error class names; files/exports.
- crypto: empty auth chain is invalid; unknown auth-link types fail closed.
- hashing: drop ES2023 toSorted (runtime compat); validate layout options;
  realm-safe buffer check; document keccak256Hash ordering sensitivity.
- single-sign-on-client: getIdentity validates identity shape and clears/returns
  null on malformed stored values instead of throwing.
- crypto-middleware/crypto-fetch: document that authMetadata is authenticated
  case-insensitively only (lowercased signed-fetch payload).

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix: security and robustness hardening across core libs

Verdict: APPROVE — no P0 or P1 issues found. This is a thorough, well-structured security hardening PR with excellent test coverage (935 tests, 0 failures) and clear changeset documentation. CI passes.

Git Convention Check (ADR-6)

  • PR title: fix: security and robustness hardening across core libs
  • Branch: fix/security-robustness-hardening

Security Review

All seven hardening areas were audited. The fixes are correct:

  • URN CID pattern (/^[A-Za-z0-9]+$/) is correct for CIDv0 (base58btc) and CIDv1 (base32). Path-traversal payloads are rejected before URL concatenation.
  • Bearer token timing — SHA-256 digests are always 32 bytes, eliminating the length-based short-circuit timing leak. createHash('sha256').update(value).digest() is correct.
  • ERROR_VALIDATOR throw is properly caught by validateSignature's try/catch (lines 38-49) and converted to { ok: false, message }. The old { error } return silently reset authority — a real security hole now fixed.
  • Third-party URN fail-closed — no bypass found. URN classification is based on the type field from parseUrn, which cleanly distinguishes blockchain-collection-v2 from blockchain-collection-third-party.
  • SSO isValidIdentity — no prototype pollution risk. JSON.parse creates plain objects.
  • ✅ No hardcoded secrets, injection vulnerabilities, or missing critical input validation found.

Consumer Impact

The two consumer-visible behavior changes are clearly documented in the PR description and changesets:

  1. Subgraph third-party fail-closed — only affects consumers using the subgraph checker (not the on-chain checker) for third-party items. Correctly marked as patch since the previous behavior was a security bug (silently skipping ownership checks).
  2. Empty auth chain rejection — previously isValidAuthChain([]) returned true, which is a correctness bug. Low practical risk since empty chains are always invalid in real usage.

The @dcl/single-sign-on-client change (shape validation in getIdentity) converts a throw-on-malformed-data into a graceful null return, which is strictly safer for callers.

Findings (P2 — Minor, non-blocking)

[P2] Code duplication: isIntegerCoordinate
libs/content-validator/src/validations/access/on-chain/scenes.ts and subgraph/scenes.ts define identical isIntegerCoordinate functions. Consider extracting to a shared util to avoid future drift.

[P2] Code duplication: isUint8ArrayLike
libs/hashing/src/_layout.ts and libs/hashing/src/node.ts have byte-identical definitions. Could be extracted to a shared _util.ts module within the hashing package.

[P2] safeParseUrn JSDoc is now slightly misleading
Now that parseUrn itself returns null on malformed input, the safeParseUrn wrapper in content-validator/src/utils.ts is partially redundant for parse failures. It still provides defense against deeper internal resolver failures (e.g., network errors), but the comment should be updated to reflect the shifted purpose.

[P2] parseJson body-size guard: edge case with non-numeric content-length
In libs/http-commons/src/utils/parsing.ts, a non-numeric content-length (e.g. "abc") falls through the guard since Number("abc") is NaN and the !isNaN() check skips it. The missing-header limitation is already documented inline; consider noting the non-numeric case too.

[P2] isUint8ArrayLike type narrowing is broader than the annotation
The guard narrows to content is Uint8Array but actually accepts any ArrayBufferView except DataView (e.g., Float32Array). In practice this is safe because the TypeScript LayoutInput type constrains callers to Uint8Array, but the runtime check is looser than the type assertion suggests.

What's Done Well

  • Every behavior change has test coverage with clear, descriptive test names
  • Changeset descriptions are detailed and explain not just what changed but why
  • The "Intentionally not changed" section in the PR body shows thoughtful scope control
  • The < to <= boundary fix for ADR_74_TIMESTAMP is a subtle correctness fix caught proactively
  • Bearer token middleware SHA-256 approach is a clean solution to the timing side-channel

Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U9B5LS5GX>) via Slack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants