fix: security and robustness hardening across core libs - #48
fix: security and robustness hardening across core libs#48LautaroPetaccio wants to merge 2 commits into
Conversation
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
left a comment
There was a problem hiding this comment.
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_VALIDATORthrow is properly caught byvalidateSignature'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
typefield fromparseUrn, which cleanly distinguishesblockchain-collection-v2fromblockchain-collection-third-party. - ✅ SSO
isValidIdentity— no prototype pollution risk.JSON.parsecreates 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:
- Subgraph third-party fail-closed — only affects consumers using the subgraph checker (not the on-chain checker) for third-party items. Correctly marked as
patchsince the previous behavior was a security bug (silently skipping ownership checks). - Empty auth chain rejection — previously
isValidAuthChain([])returnedtrue, 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 forADR_74_TIMESTAMPis 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
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-commonsminor;decentraland-crypto-fetchauto-bumps as a@dcl/cryptodependent).@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.@dcl/crypto-middleware— metadata gap documented, not changed. The signed-fetch payload is lowercased by wire-protocol convention shared with all Decentraland signers, soauthMetadatais authenticated case-insensitively only. Removing the lowercasing would break interop, so this is documented (JSDoc + existing README) rather than changed.Changes by package
parseUrn/resolveUrlFromUrnreturnnullinstead of throwing on malformed input (bad%-escapes, non-URL input, and invalid/out-of-boundsLANDpositions that threw viaBigInt); entity resolver restrictsbaseUrltohttp(s)and validatescid(blocksjavascript:, open-redirect, path-traversal); case-insensitive network check; strict parcel coordinates.safeParseUrnin common access checks; unknown-entity-type guard; inclusive ADR-74 boundary; strict scene pointer parsing.PayloadTooLargeError(413); log stack on 500s (server-side only, not leaked); digest-based constant-time bearer comparison + case-insensitive scheme; strict pagination; error-classnames;files/exportsinpackage.json.toSorted(older-runtime compat); layout-option validation; realm-safe buffer check; documentedkeccak256Hashordering sensitivity.getIdentityvalidates identity shape and clears/returnsnullon malformed stored values instead of throwing.Intentionally not changed
baseUrl— arbitrary https gateways are that parameter's legitimate purpose; blocking hosts would need an allowlist. Onlyjavascript:/malformed/traversal are blocked..find→Setperf tweak — low value / ambiguous.Test plan
pnpm -r buildpnpm -r test— 935 tests passpnpm -r lint