Skip to content

Conversation

@ricardogarim
Copy link
Contributor

@ricardogarim ricardogarim commented Sep 4, 2025

Works along with RocketChat/Rocket.Chat#36873.

Summary by CodeRabbit

  • New Features
    • Signature-based authorization for federation requests, verifying requester identity.
    • Enforcement of per-room server ACLs and history visibility for cross-server event access.
    • Standardized error responses for federation endpoints with clear codes and HTTP statuses (401/403/500).
    • Pre-request authorization on event fetch routes to block unauthorized or forbidden access early.

@codecov-commenter
Copy link

codecov-commenter commented Sep 4, 2025

Codecov Report

❌ Patch coverage is 28.57143% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.10%. Comparing base (9d5b2b1) to head (f02fdbb).

Files with missing lines Patch % Lines
packages/room/src/manager/event-wrapper.ts 28.57% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #161      +/-   ##
==========================================
- Coverage   81.18%   81.10%   -0.08%     
==========================================
  Files          63       63              
  Lines        4676     4683       +7     
==========================================
+ Hits         3796     3798       +2     
- Misses        880      885       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ricardogarim ricardogarim changed the base branch from main to feat/get-event-by-id-route September 11, 2025 00:00
@ricardogarim ricardogarim marked this pull request as ready for review September 11, 2025 02:54
@ricardogarim ricardogarim changed the title feat: adds ACL related methods to EventAuthorizationService feat: adds event ACL Sep 11, 2025
@ricardogarim ricardogarim requested a review from ggazzo September 11, 2025 17:05
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 13, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds cryptographic, header-based federation event authorization with server ACL and history visibility checks; wires EventAuthorizationService into SDK exports and homeserver routes via middleware; introduces standardized error codes; updates state service server extraction; and adds type guards for ACL/history visibility events.

Changes

Cohort / File(s) Summary
Federation SDK API wiring
packages/federation-sdk/src/index.ts, packages/federation-sdk/src/utils/response-codes.ts
Exposes EventAuthorizationService via DI in getAllServices and HomeserverServices; publicly exports errCodes used by middleware.
Event authorization service
packages/federation-sdk/src/services/event-authorization.service.ts
Implements signature verification from X-Matrix headers, public key retrieval (local/remote), per-event access checks using room ACLs and history visibility, and a combined canAccessEventFromAuthorizationHeader method returning standardized error codes.
State service adjustments
packages/federation-sdk/src/services/state.service.ts
Reworks getServersInRoom to async/await with stricter member format validation and explicit errors.
Homeserver integration
packages/homeserver/src/controllers/federation/transactions.controller.ts, packages/homeserver/src/middlewares/acl.middleware.ts
Adds canAccessEvent middleware using EventAuthorizationService to gate GET /_matrix/federation/v1/event/:eventId; resolves service from container; maps errCodes to HTTP status/payload.
Room event type guards
packages/room/src/manager/event-wrapper.ts
Adds isServerAclEvent and isHistoryVisibilityEvent type guards to narrow state events.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor RS as Remote Server
  participant HS as Homeserver Controller
  participant MW as Middleware (canAccessEvent)
  participant EAS as EventAuthorizationService
  participant SS as StateService
  participant ES as EventService
  participant KR as KeyRepository
  participant RS2 as Remote HS (Key fetch)

  RS->>HS: GET /_matrix/federation/v1/event/:eventId + Authorization
  HS->>MW: onBeforeHandle (eventId, headers, method, uri)
  MW->>EAS: canAccessEventFromAuthorizationHeader(eventId, auth, method, uri)
  EAS->>EAS: extractSignaturesFromHeader / validateAuthorizationHeader
  alt signer key known locally
    EAS->>KR: getPublicKey(server, keyId)
  else fetch from remote
    EAS->>RS2: GET /_matrix/key/v2/server
    RS2-->>EAS: server keys
    EAS->>KR: persist/update key
  end
  EAS->>ES: load event(eventId)
  EAS->>SS: resolve room state (members, ACL, visibility)
  EAS-->>MW: {authorized}|{authorized:false, errCode}
  alt authorized
    MW-->>HS: continue
    HS->>ES: getEvent(eventId)
    ES-->>HS: event
    HS-->>RS: 200 OK + event
  else unauthorized/forbidden/error
    MW-->>RS: status (401/403/500) + {errcode, error}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rodrigok
  • ggazzo

Poem

A rabbit signs with careful paws,
Headers whisper secret laws.
Keys are fetched, ACLs read,
World-readable paths to tread.
If doors are barred, we hop away—
Errcodes tell what we can’t say.
Thump-thump: secure, we save the day! 🐇🔑

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: adds event ACL" succinctly and accurately summarizes the primary change — introducing event access control (server ACLs and authorization) across the federation SDK, middleware, and controller wiring. It is concise, focused, and clearly indicates a feature addition. A reviewer scanning the history would understand the main intent of the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c43cce and f02fdbb.

📒 Files selected for processing (7)
  • packages/federation-sdk/src/index.ts (4 hunks)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (1 hunks)
  • packages/federation-sdk/src/utils/response-codes.ts (1 hunks)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (3 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
  • packages/room/src/manager/event-wrapper.ts (1 hunks)

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Comment @coderabbitai help to get the list of available commands and usage tips.

@ricardogarim ricardogarim changed the base branch from feat/get-event-by-id-route to main September 13, 2025 21:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
packages/federation-sdk/src/services/staging-area.service.ts (3)

359-372: Avoid spreading untrusted event content; sanitize or whitelist keys before emit.

Blindly spreading stagedEvent.event.content can leak unexpected keys and enable prototype-pollution vectors (e.g., __proto__). Sanitize first or whitelist allowed fields.

Apply a minimal sanitization before spreading:

- content: {
-   ...stagedEvent.event.content,
+ const safe = Object.fromEntries(
+   Object.entries(stagedEvent.event.content ?? {}).filter(
+     ([k]) => k !== '__proto__' && k !== 'constructor' && k !== 'prototype',
+   ),
+ );
+ content: {
+   ...safe,
    body: stagedEvent.event.content?.body as string,
    msgtype: stagedEvent.event.content?.msgtype as string,
    'm.relates_to': stagedEvent.event.content?.['m.relates_to'] as {
      rel_type: 'm.replace' | 'm.annotation' | 'm.thread';
      event_id: string;
    },
    'm.new_content': stagedEvent.event.content?.['m.new_content'] as {
      body: string;
      msgtype: string;
    },
    formatted_body: (stagedEvent.event.content?.formatted_body || '') as string,
  },

506-515: -Bug: Set.prototype.difference does not exist (runtime error).

This will throw at runtime on Node/JS. Replace with explicit symmetric difference.

-const setOrUnsetPowerLevels = new Set(
-  usersInNewPowerLevelEvent,
-).difference(new Set(usersInOldPowerLevelEvent));
+const newSet = new Set(usersInNewPowerLevelEvent);
+const oldSet = new Set(usersInOldPowerLevelEvent);
+const setOrUnsetPowerLevels = new Set([
+  ...[...newSet].filter((u) => !oldSet.has(u)),
+  ...[...oldSet].filter((u) => !newSet.has(u)),
+]);

204-207: Don’t hardcode 'm.room.message' for auth queries.

Use the incoming event’s type; otherwise non-message events pass wrong auth context.

-const authEvents = await this.eventService.getAuthEventIds(
-  'm.room.message',
+const authEvents = await this.eventService.getAuthEventIds(
+  event.event.type,
   { roomId: event.roomId, senderId: event.event.sender },
);
packages/federation-sdk/src/services/signature-verification.service.ts (3)

97-131: Public key fetch is insecure and has no TTL; also ignores old keys.

  • Trusts key response without verifying the server key response per Matrix spec.
  • No timeout; potential hang.
  • Caches by ${server}:${keyId} but stores whole key set; wastes memory and misses reuse.
  • Ignores old_verify_keys/expired_ts; breaks historical signature checks.
 interface KeyData {
   server_name: string;
+  valid_until_ts?: number;
   verify_keys: {
     [keyId: string]: {
       key: string;
     };
   };
   old_verify_keys?: {
     [keyId: string]: {
       key: string;
       expired_ts: number;
     };
   };
 }
 
 export class SignatureVerificationService {
   private get logger() {
     return createLogger('SignatureVerificationService');
   }
-  private cachedKeys = new Map<string, KeyData>();
+  private cachedKeys = new Map<string, { keyData: KeyData; fetchedAt: number }>();
 
@@
-  async getOrFetchPublicKey(
-    serverName: string,
-    keyId: string,
-  ): Promise<KeyData | null | undefined> {
-    const cacheKey = `${serverName}:${keyId}`;
+  async getOrFetchPublicKey(
+    serverName: string,
+    keyId: string,
+  ): Promise<KeyData | null> {
+    const cacheKey = serverName;
 
     if (this.cachedKeys.has(cacheKey)) {
-      return this.cachedKeys.get(cacheKey);
+      return this.cachedKeys.get(cacheKey)!.keyData;
     }
 
     try {
-      const response = await fetch(
-        `https://${serverName}/_matrix/key/v2/server`,
-      );
+      const controller = new AbortController();
+      const t = setTimeout(() => controller.abort(), 5000);
+      const response = await fetch(`https://${serverName}/_matrix/key/v2/server`, {
+        headers: { Accept: 'application/json' },
+        signal: controller.signal,
+      });
+      clearTimeout(t);
 
       if (!response.ok) {
         this.logger.error(
           `Failed to fetch keys from ${serverName}: ${response.status}`,
         );
         return null;
       }
 
       const keyData = (await response.json()) as KeyData;
 
-      this.cachedKeys.set(cacheKey, keyData);
+      // TODO: verify the server response signature per spec and reject if invalid
+      this.cachedKeys.set(cacheKey, { keyData, fetchedAt: Date.now() });
 
       return keyData;
     } catch (error: any) {
       this.logger.error(
         `Error fetching public key: ${error.message}`,
         error.stack,
       );
       return null;
     }
   }

Follow-ups:

  • In verifySignature, if verify_keys[keyId] is missing, check old_verify_keys[keyId] and ensure event.origin_server_ts <= expired_ts.
  • Optionally honor valid_until_ts to evict stale entries and refetch.

I can wire these changes and add unit tests if helpful.


60-71: Fallback to old keys for historical events.

If verify_keys[keyId] is absent but old_verify_keys[keyId] exists and the event timestamp pre-dates expired_ts, use the old key.

-const keyData = await this.getOrFetchPublicKey(originServer, keyId);
-if (!keyData || !keyData.verify_keys[keyId]) {
+const keyData = await this.getOrFetchPublicKey(originServer, keyId);
+const current = keyData?.verify_keys?.[keyId]?.key;
+const old = keyData?.old_verify_keys?.[keyId];
+if (!current && old && typeof (event as any).origin_server_ts === 'number' && (event as any).origin_server_ts <= old.expired_ts) {
+  publicKey = old.key;
+} else if (!current) {
   this.logger.warn(`Public key not found for ${originServer}:${keyId}`);
   return false;
-}
-publicKey = keyData.verify_keys[keyId].key;
+}
+publicKey = current ?? publicKey;

75-85: Use canonical JSON for signature verification — don’t use JSON.stringify

JSON.stringify is not canonical; Matrix/cryptographic signatures require a canonical JSON form. Replace the line
const canonicalJson = JSON.stringify(eventToVerify);
with a canonicalization call from the same canonicalizer the signer uses (e.g. canonicalize(eventToVerify) or toCanonicalJSONString(eventToVerify)). I couldn’t find an existing canonical JSON utility in the repo (only a comment in event-authorization.service.ts mentioning canonical json validation), so add/import a canonicalizer and use it in packages/federation-sdk/src/services/signature-verification.service.ts.

packages/homeserver/src/homeserver.module.ts (1)

72-73: Bug: enableThumbnails is always true.

X === 'true' || true short-circuits to true. Respect the env var.

-enableThumbnails: process.env.MEDIA_ENABLE_THUMBNAILS === 'true' || true,
+enableThumbnails:
+  process.env.MEDIA_ENABLE_THUMBNAILS === undefined
+    ? true
+    : process.env.MEDIA_ENABLE_THUMBNAILS === 'true',
♻️ Duplicate comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

804-807: Empty server part should be impossible; prefer enforcing or erroring.

Changing ! to ?? '' hides data issues and echoes a prior review concern.

-return this.getMembersOfRoom(roomId).then((members) =>
-  members.map((member) => member.split(':').pop() ?? ''),
-);
+return this.getMembersOfRoom(roomId).then((members) => {
+  const servers = members.map((m) => m.split(':').pop());
+  if (servers.some((s) => !s)) {
+    throw new Error('Malformed Matrix IDs in room membership');
+  }
+  return servers as string[];
+});
🧹 Nitpick comments (10)
packages/federation-sdk/src/services/staging-area.service.ts (3)

286-288: Use logger instead of console.log.

Keep logs consistent and structured.

-console.log('Skipping persistence stage, persisted in previous stage'); // TODO: revisit
+this.logger.debug('Skipping persistence stage, persisted in previous stage'); // TODO: revisit

171-180: Backoff is linear, not exponential (comment mismatch).

If exponential is desired, fix the wait calculation.

-}, 1000 * trackedEvent.retryCount); // Exponential backoff
+}, 1000 * (2 ** (trackedEvent.retryCount - 1))); // Exponential backoff

81-83: Re-enable locking to prevent duplicate concurrent processing.

The @Lock is commented; concurrent queue replays can process the same room/event simultaneously.

-// @Lock({ timeout: 10000, keyPath: 'event.room_id' })
+@Lock({ timeout: 10000, keyPath: 'event.room_id' })
packages/homeserver/src/controllers/federation/transactions.controller.ts (1)

1-6: Remove unused import.

Context is not used.

-import { Context, Elysia } from 'elysia';
+import { Elysia } from 'elysia';
packages/federation-sdk/src/services/state.service.ts (1)

262-271: Type the return and simplify iteration.

Specify the return type and leverage state.values().

-async getStateEventsByType(roomId: string, type: PduType) {
-  const state = await this.getFullRoomState(roomId);
-  const events = [];
-  for (const [, event] of state) {
-    if (event.type === type) {
-      events.push(event);
-    }
-  }
-  return events;
-}
+async getStateEventsByType(roomId: string, type: PduType): Promise<PersistentEventBase[]> {
+  const state = await this.getFullRoomState(roomId);
+  return Array.from(state.values()).filter((e) => e.type === type);
+}
packages/homeserver/src/middlewares/acl.middleware.ts (1)

32-33: Read the Authorization header case‑insensitively from Request.headers.

Headers are case-insensitive; prefer request.headers.get('authorization') over trusting a raw headers.authorization snapshot.

-		const authorizationHeader = headers.authorization || '';
+		const authorizationHeader =
+			request.headers.get('authorization') ??
+			headers.authorization ??
+			'';
packages/federation-sdk/src/services/event-authorization.service.ts (4)

206-213: Avoid full room state fetch for history visibility; query the single state event.

getFullRoomState(roomId) is heavier than needed. Fetch only m.room.history_visibility.

-			const roomState = await this.stateService.getFullRoomState(roomId);
-			const historyVisibility = this.getHistoryVisibility(roomState);
+			const [hv] = await this.stateService.getStateEventsByType(
+				roomId,
+				'm.room.history_visibility',
+			);
+			const historyVisibility = this.extractHistoryVisibility(hv);

Add:

private extractHistoryVisibility(
	stateEvent?: { getContent?: () => any; content?: any },
): string {
	const content = stateEvent?.getContent?.() ?? stateEvent?.content;
	return content?.history_visibility || 'shared';
}

228-238: Default should be 'shared', not 'joined'.

Spec says when absent/unknown, history_visibility defaults to shared. Adjust to avoid misleading behavior/comments. (spec.matrix.org)

-		return 'joined'; // default per Matrix spec
+		return 'shared'; // default per Matrix spec

351-370: Dynamic regex from user-controlled patterns: cap length to reduce DoS risk.

Though you escape meta-chars, very long patterns can still be expensive. Add a small length guard.

 	private matchesServerPattern(serverName: string, pattern: string): boolean {
+		// sanity cap to reduce pathological inputs
+		if (pattern.length > 255) {
+			this.logger.warn(`ACL pattern too long; ignoring: length=${pattern.length}`);
+			return false;
+		}

240-288: Return-shape is clear and narrow. Consider unit tests for each branch.

Please add tests for:

  • invalid/missing signature → M_UNAUTHORIZED
  • denied by ACL (deny hit, allow miss, allow list empty) → M_FORBIDDEN
  • allowed when server is in room
  • allowed when history is world_readable

I can scaffold these tests to recover the Codecov hit — want me to open a follow-up PR?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea23573 and 8450aa4.

📒 Files selected for processing (11)
  • packages/federation-sdk/src/container.ts (1 hunks)
  • packages/federation-sdk/src/index.ts (3 hunks)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/media.service.ts (0 hunks)
  • packages/federation-sdk/src/services/signature-verification.service.ts (1 hunks)
  • packages/federation-sdk/src/services/staging-area.service.ts (1 hunks)
  • packages/federation-sdk/src/services/state.service.ts (3 hunks)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (3 hunks)
  • packages/homeserver/src/controllers/media.controller.ts (0 hunks)
  • packages/homeserver/src/homeserver.module.ts (1 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/homeserver/src/controllers/media.controller.ts
  • packages/federation-sdk/src/services/media.service.ts
🧰 Additional context used
🧬 Code graph analysis (6)
packages/homeserver/src/middlewares/acl.middleware.ts (2)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (179-226)
packages/federation-sdk/src/index.ts (1)
  • EventAuthorizationService (40-40)
packages/homeserver/src/homeserver.module.ts (1)
packages/homeserver/src/controllers/federation/rooms.controller.ts (1)
  • roomPlugin (5-117)
packages/federation-sdk/src/services/event-authorization.service.ts (2)
packages/federation-sdk/src/services/signature-verification.service.ts (1)
  • SignatureVerificationService (20-132)
packages/federation-sdk/src/services/config.service.ts (1)
  • serverName (92-94)
packages/federation-sdk/src/container.ts (1)
packages/federation-sdk/src/index.ts (4)
  • EventFetcherService (44-44)
  • EventStateService (41-41)
  • EventService (49-49)
  • EventAuthorizationService (40-40)
packages/homeserver/src/controllers/federation/transactions.controller.ts (2)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (179-226)
packages/homeserver/src/middlewares/acl.middleware.ts (1)
  • canAccessEvent (28-51)
packages/federation-sdk/src/services/state.service.ts (4)
packages/federation-sdk/src/index.ts (1)
  • State (22-22)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/manager/event-wrapper.ts (3)
  • roomId (72-74)
  • type (68-70)
  • event (98-108)
packages/room/src/types/v3-11.ts (1)
  • PduType (45-45)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 363-363: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (7)
packages/homeserver/src/homeserver.module.ts (1)

120-121: LGTM: plugin wiring for rooms.

packages/homeserver/src/controllers/federation/transactions.controller.ts (2)

66-67: LGTM: route-level ACL via beforeHandle.

Authorization is enforced consistently ahead of handler execution.


59-63: Minor: ensure origin on returned PDU matches spec.

Current value is local server name; confirm this aligns with your spec target for GET /event.

If required by your Matrix compatibility target, we can adjust; otherwise ignore.

packages/federation-sdk/src/services/state.service.ts (1)

20-20: Exporting State type is fine.

packages/federation-sdk/src/services/event-authorization.service.ts (1)

112-177: Minor: allow missing destination per spec; your code already does — keep.

destination may be omitted by older servers; you fallback to local server name. This is correct. (spec.matrix.org)

packages/federation-sdk/src/index.ts (1)

95-109: Public API changed — HomeserverServices.media removed; federationAuth added. Verify downstream consumers.

  • TS search found no internal .media usages. HomeserverServices defined at packages/federation-sdk/src/index.ts (lines ~95–109); getAllServices() exported there (line ~218). packages/federation-sdk/src/services/config.service.ts:getMediaConfig() returns this.config.media (line 105) — that’s config, not the removed service.
  • Action: update external consumers/docs and bump or gate the release to avoid breaking users.
packages/federation-sdk/src/container.ts (1)

93-97: Preserve string DI token 'EventFetcherService' for back-compat

registerSingleton(EventFetcherService) changes the DI token from the string "EventFetcherService" to the class — any constructor using @Inject('EventFetcherService') will stop resolving. Keep a temporary string alias while migrating injections.

File: packages/federation-sdk/src/container.ts (around lines 93–97)

 container.registerSingleton(StateService);
-	container.registerSingleton(EventFetcherService);
+	// Prefer class token…
+	container.registerSingleton(EventFetcherService);
+	// …but keep string alias for back-compat while migrating injections.
+	container.register('EventFetcherService', {
+		useFactory: (c) => c.resolve(EventFetcherService),
+	});

rg search skipped files; verify whether any code uses @Inject('EventFetcherService') or the literal 'EventFetcherService' token and add the alias above if found.

Comment on lines +34 to +15
const uri = new URL(request.url).pathname;

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Signature verification must include the query string in the signed URI.

Per the Matrix federation auth spec, the signed uri includes ? and query parameters. Use pathname + search, not just pathname, to avoid false negatives. (spec.matrix.org)

Apply:

-		const uri = new URL(request.url).pathname;
+		const url = new URL(request.url);
+		const uri = `${url.pathname}${url.search}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const uri = new URL(request.url).pathname;
const url = new URL(request.url);
const uri = `${url.pathname}${url.search}`;
🤖 Prompt for AI Agents
In packages/homeserver/src/middlewares/acl.middleware.ts around lines 34-35, the
code builds the signed URI using only new URL(request.url).pathname which omits
query parameters; update it to include the query string by using the URL
object's pathname plus its search component (e.g. url.pathname + url.search or
pathname + (search || '')) so the signed URI matches the Matrix federation spec
and includes the '?' and any query parameters when verifying signatures.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/federation-sdk/src/services/event-authorization.service.ts (1)

184-211: Membership/ACL checks should compare host-only (ignore port) to avoid false denies/allows.

Using serverName with ports will fail serversInRoom.includes(...) and ACL matches; strip the port first.

Apply this diff:

-	private async canAccessEvent(
+	private async canAccessEvent(
 		eventId: string,
 		serverName: string,
 	): Promise<boolean> {
 		try {
 			const event = await this.eventService.getEventById(eventId);
@@
-			const isServerAllowed = await this.checkServerAcl(roomId, serverName);
+			const originHost = this.stripPort(serverName);
+			const isServerAllowed = await this.checkServerAcl(roomId, originHost);
 			if (!isServerAllowed) {
 				this.logger.warn(
 					`Server ${serverName} is denied by room ACL for room ${roomId}`,
 				);
 				return false;
 			}
 
 			const serversInRoom = await this.stateService.getServersInRoom(roomId);
-			if (serversInRoom.includes(serverName)) {
+			if (serversInRoom.includes(originHost)) {
 				this.logger.debug(`Server ${serverName} is in room, allowing access`);
 				return true;
 			}
🧹 Nitpick comments (3)
packages/federation-sdk/src/services/event-authorization.service.ts (3)

114-171: Normalize inputs for signature verification (method, URI path+query).

To reduce signature-mismatch risk across clients, uppercase the method and ensure uri is path+query (not a full URL).

Apply this minimal diff and verify that validateAuthorizationHeader expects path+query:

-			const isValid = await validateAuthorizationHeader(
+			const methodUpper = method.toUpperCase();
+			let uriForSig = uri;
+			if (!uri.startsWith('/')) {
+				try {
+					const u = new URL(uri);
+					uriForSig = `${u.pathname}${u.search}`;
+				} catch {
+					// leave as-is; caller must supply path+query
+				}
+			}
+			const isValid = await validateAuthorizationHeader(
 				origin,
 				publicKey,
 				actualDestination,
-				method,
-				uri,
+				methodUpper,
+				uriForSig,
 				signature,
 				body,
 			);

If extractSignaturesFromHeader can return multiple signatures, prefer the one whose destination matches this.configService.serverName. Want me to adjust for multi-sig?

Also applies to: 162-171


233-243: LGTM: default history visibility of 'joined' is reasonable.

Optional: narrow return type to the visibility union to prevent typos in call sites.

type HistoryVisibility = 'invited' | 'joined' | 'shared' | 'world_readable';
private getHistoryVisibility(roomState: Map<any, any>): HistoryVisibility { ... }

300-307: Behavior when no ACL event exists is correct; consider adding tests.

Returning true when no m.room.server_acl is present aligns with current “no ACL state” behavior.

Would you like test vectors for:

  • allow missing vs empty,
  • host vs host:port,
  • IPv4/IPv6 literals with allow_ip_literals true/false,
  • deny precedence over allow,
  • wildcard edge cases (*, *.example.org, example.*)? I can add a spec-driven test file to improve the patch coverage reported by Codecov.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8450aa4 and d6a7f91.

📒 Files selected for processing (1)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.421Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.421Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
packages/federation-sdk/src/services/config.service.ts (1)
  • serverName (92-94)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 368-368: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (2)
packages/federation-sdk/src/services/event-authorization.service.ts (2)

205-209: LGTM: fall back to history visibility for world-readable rooms.

Access allowed for world_readable when not in room is correct.


245-293: LGTM: clear auth result mapping (401 vs 403 vs unknown).

Return structure and logging look good.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/federation-sdk/src/services/event-authorization.service.ts (4)

184-211: Use host-only for ACL and membership checks (ignore ports).

Spec-wise ACL and membership comparisons operate on host. Using serverName with :port causes false denies/allows.

Apply:

 			const roomId = event.event.room_id;
+			const originHost = this.stripPort(serverName);
 
-			const isServerAllowed = await this.checkServerAcl(roomId, serverName);
+			const isServerAllowed = await this.checkServerAcl(roomId, originHost);
@@
-			const serversInRoom = await this.stateService.getServersInRoom(roomId);
-			if (serversInRoom.includes(serverName)) {
+			const serversInRoom = await this.stateService.getServersInRoom(roomId);
+			if (serversInRoom.includes(originHost)) {

295-354: ACL semantics: treat missing allow as allow-all; operate on host-only; fix IP-literal detection.

Current code denies-all when allow is absent and matches against host:port. This diverges from spec and can over-block.

Apply:

 		const serverAclContent = aclEvent.getContent() as {
 			allow?: string[];
 			deny?: string[];
 			allow_ip_literals?: boolean;
 		};
-		const {
-			allow = [],
-			deny = [],
-			allow_ip_literals = true,
-		} = serverAclContent;
+		const allow = Array.isArray(serverAclContent.allow)
+			? serverAclContent.allow
+			: undefined;
+		const deny = Array.isArray(serverAclContent.deny) ? serverAclContent.deny : [];
+		const allow_ip_literals = serverAclContent.allow_ip_literals ?? true;
+		const hostOnly = this.stripPort(serverName);
 
-		const isIpLiteral =
-			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) ||
-			/^\[.*\](:\d+)?$/.test(serverName); // IPv6
+		const isIpLiteral =
+			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOnly) ||
+			/^\[[^\]]+\]$/.test(hostOnly); // IPv6
 
 		if (isIpLiteral && !allow_ip_literals) {
 			this.logger.debug(`Server ${serverName} denied: IP literals not allowed`);
 			return false;
 		}
 
 		for (const pattern of deny) {
-			if (this.matchesServerPattern(serverName, pattern)) {
+			if (this.matchesServerPattern(hostOnly, pattern)) {
 				this.logger.debug(
 					`Server ${serverName} matches deny pattern: ${pattern}`,
 				);
 				return false;
 			}
 		}
 
-		// if allow list is empty, deny all servers (as per Matrix spec)
-		// empty allow list means no servers are allowed
-		if (allow.length === 0) {
-			this.logger.debug(`Server ${serverName} denied: allow list is empty`);
-			return false;
-		}
+		// If 'allow' is explicitly present and empty, deny all. If missing, allow-all (subject to 'deny').
+		if (allow !== undefined && allow.length === 0) {
+			this.logger.debug(`Server ${serverName} denied: allow list is explicitly empty`);
+			return false;
+		}
 
-		for (const pattern of allow) {
-			if (this.matchesServerPattern(serverName, pattern)) {
-				this.logger.debug(
-					`Server ${serverName} matches allow pattern: ${pattern}`,
-				);
-				return true;
-			}
-		}
-
-		this.logger.debug(`Server ${serverName} not in allow list`);
-		return false;
+		if (allow !== undefined) {
+			for (const pattern of allow) {
+				if (this.matchesServerPattern(hostOnly, pattern)) {
+					this.logger.debug(`Server ${serverName} matches allow pattern: ${pattern}`);
+					return true;
+				}
+			}
+			this.logger.debug(`Server ${serverName} not in explicit allow list`);
+			return false;
+		}
+		// No explicit allow list -> allowed (not denied above)
+		return true;

356-375: ReDoS risk: avoid building RegExp from untrusted ACL patterns; implement simple '*' wildcard match.

Regex-from-input enables catastrophic backtracking; spec only requires '*' wildcard.

Apply:

 	private matchesServerPattern(serverName: string, pattern: string): boolean {
-		if (serverName === pattern) {
-			return true;
-		}
-
-		let regexPattern = pattern
-			.replace(/[.+^${}()|[\]\\]/g, '\\$&')
-			.replace(/\*/g, '.*')
-			.replace(/\?/g, '.');
-
-		regexPattern = `^${regexPattern}$`;
-
-		try {
-			const regex = new RegExp(regexPattern);
-			return regex.test(serverName);
-		} catch (error) {
-			this.logger.warn(`Invalid ACL pattern: ${pattern}`, error);
-			return false;
-		}
+		// Exact match or match-all
+		if (serverName === pattern || pattern === '*') return true;
+		// Support '*' only
+		const parts = pattern.split('*');
+		let pos = 0;
+		if (!pattern.startsWith('*')) {
+			const first = parts.shift()!;
+			if (!serverName.startsWith(first)) return false;
+			pos = first.length;
+		}
+		for (let i = 0; i < parts.length - 1; i++) {
+			const seg = parts[i];
+			if (!seg) continue;
+			const idx = serverName.indexOf(seg, pos);
+			if (idx === -1) return false;
+			pos = idx + seg.length;
+		}
+		const last = parts[parts.length - 1] ?? '';
+		if (!pattern.endsWith('*')) {
+			return serverName.endsWith(last);
+		}
+		return true;
 	}

376-376: Add host/port normalization helper.

Required by fixes above.

Add inside the class:

+	private stripPort(serverName: string): string {
+		// [IPv6]:port -> [IPv6]
+		if (serverName.startsWith('[')) {
+			const m = serverName.match(/^\[([^\]]+)\](?::\d+)?$/);
+			return m ? `[${m[1]}]` : serverName;
+		}
+		// host:port -> host
+		return serverName.replace(/:\d+$/, '');
+	}
🧹 Nitpick comments (3)
packages/homeserver/src/controllers/federation/transactions.controller.ts (1)

68-68: Signature check may use wrong URI (drop query) — include path + query in canonical request.

Middleware builds uri with new URL(request.url).pathname, excluding search. Signatures usually cover the full request-target (path + query). Pass path + query to avoid false 401 on signed requests with query params.

Update in packages/homeserver/src/middlewares/acl.middleware.ts:

const u = new URL(request.url);
const uri = u.pathname + u.search; // include query string
packages/federation-sdk/src/services/event-authorization.service.ts (2)

129-137: Destination comparison should tolerate port/case differences (normalize or rely on validator).

Early-rejecting when destination !== serverName can spuriously fail if one has a port. Normalize host-only and case, or defer to validateAuthorizationHeader.

Apply:

-			if (
-				!origin ||
-				!key ||
-				!signature ||
-				(destination && destination !== this.configService.serverName)
-			) {
-				return;
-			}
+			if (!origin || !key || !signature) {
+				return;
+			}
+			if (
+				destination &&
+				this.stripPort(destination.toLowerCase()) !==
+					this.stripPort(this.configService.serverName.toLowerCase())
+			) {
+				return;
+			}

(Add stripPort helper below.)


245-293: Add tests for end-to-end authorization outcomes.

Critical paths lack coverage (Codecov). Add unit tests for:

  • valid signature + in-room
  • valid signature + world_readable
  • valid signature + ACL deny
  • missing/invalid signature → 401
  • dest mismatch → 401
  • error path → 500 mapping

I can scaffold a test matrix for these cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6a7f91 and f8291ee.

📒 Files selected for processing (7)
  • packages/federation-sdk/src/container.ts (1 hunks)
  • packages/federation-sdk/src/index.ts (3 hunks)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (3 hunks)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (3 hunks)
  • packages/homeserver/src/homeserver.module.ts (1 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/homeserver/src/middlewares/acl.middleware.ts
  • packages/homeserver/src/homeserver.module.ts
  • packages/federation-sdk/src/index.ts
  • packages/federation-sdk/src/container.ts
  • packages/federation-sdk/src/services/state.service.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.421Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.421Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (2)
packages/homeserver/src/controllers/federation/transactions.controller.ts (2)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (184-231)
packages/homeserver/src/middlewares/acl.middleware.ts (1)
  • canAccessEvent (28-51)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
packages/federation-sdk/src/services/config.service.ts (1)
  • serverName (94-96)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 368-368: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (2)
packages/homeserver/src/controllers/federation/transactions.controller.ts (1)

1-5: Wiring looks good.

Import + DI resolution for EventAuthorizationService and middleware integration are correct.

Also applies to: 16-16, 21-21

packages/federation-sdk/src/services/event-authorization.service.ts (1)

233-243: Default history_visibility may be ‘shared’, not ‘joined’.

Double-check spec default to avoid over-restricting reads when state lacks this event.

Would you like me to open a follow-up to align default and add tests?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/homeserver/src/middlewares/acl.middleware.ts (1)

11-18: Include the query string in the signed URI (spec compliance).

Use pathname + search; current code signs only the path, causing false negatives with query params.

-		const method = request.method;
-		const uri = new URL(request.url).pathname;
+		const method = request.method;
+		const url = new URL(request.url);
+		const uri = `${url.pathname}${url.search}`;
🧹 Nitpick comments (4)
packages/federation-sdk/src/utils/response-codes.ts (1)

1-20: Make errCodes type-safe and immutable (derive ErrCode).

Avoid Record<string, …>; lock keys/values and export a typed union for safer indexing downstream.

-export const errCodes: Record<
-	string,
-	{ errcode: string; error: string; status: 401 | 403 | 500 }
-> = {
+export const errCodes = {
 	M_UNAUTHORIZED: {
 		errcode: 'M_UNAUTHORIZED',
 		error: 'Invalid or missing signature',
 		status: 401,
 	},
 	M_FORBIDDEN: {
 		errcode: 'M_FORBIDDEN',
 		error: 'Access denied',
 		status: 403,
 	},
 	M_UNKNOWN: {
 		errcode: 'M_UNKNOWN',
 		error: 'Internal server error while processing request',
 		status: 500,
 	},
-};
+} as const;
+
+export type ErrCode = keyof typeof errCodes;
packages/homeserver/src/middlewares/acl.middleware.ts (2)

26-32: Harden error mapping with a safe fallback to M_UNKNOWN.

Protect against unexpected errorCode values to avoid undefined access.

-		if (!result.authorized) {
-			set.status = errCodes[result.errorCode].status;
-			return {
-				errcode: errCodes[result.errorCode].errcode,
-				error: errCodes[result.errorCode].error,
-			};
-		}
+		if (!result.authorized) {
+			const info = errCodes[result.errorCode as keyof typeof errCodes] ?? errCodes.M_UNKNOWN;
+			set.status = info.status;
+			return { errcode: info.errcode, error: info.error };
+		}

15-16: Normalize Authorization header lookup (case-insensitive).

Some frameworks may provide Authorization vs authorization. Safer to check both.

-		const authorizationHeader = headers.authorization || '';
+		const authorizationHeader =
+			headers.authorization ?? headers.Authorization ?? '';
packages/federation-sdk/src/index.ts (1)

5-5: EventAuthorizationService is registered — remove duplicate registration.
container.registerSingleton(EventAuthorizationService) is present at packages/federation-sdk/src/container.ts:100 and packages/federation-sdk/src/container.ts:103; this prevents resolve errors but the duplicate registration should be removed.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f8291ee and 3ab312a.

📒 Files selected for processing (3)
  • packages/federation-sdk/src/index.ts (4 hunks)
  • packages/federation-sdk/src/utils/response-codes.ts (1 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/federation-sdk/src/utils/response-codes.ts (1)
packages/federation-sdk/src/index.ts (1)
  • errCodes (69-69)
packages/homeserver/src/middlewares/acl.middleware.ts (2)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (184-231)
packages/federation-sdk/src/utils/response-codes.ts (1)
  • errCodes (1-20)
🔇 Additional comments (1)
packages/federation-sdk/src/index.ts (1)

69-69: LGTM: re-exporting errCodes from the SDK public API.

This aligns homeserver usage with a single source of truth for status/errcode mapping.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/federation-sdk/src/services/state.service.ts (2)

289-301: Type the return based on PduType and avoid any[] inference

Return a typed array and leverage a generic so callers get PersistentEventBase<..., T> for stronger correctness.

Apply this diff:

-async getStateEventsByType(
-  roomId: string,
-  type: PduType,
-): Promise<PersistentEventBase[]> {
-  const state = await this.getFullRoomState(roomId);
-  const events = [];
-  for (const [, event] of state) {
-    if (event.type === type) {
-      events.push(event);
-    }
-  }
-  return events;
-}
+async getStateEventsByType<T extends PduType>(
+  roomId: string,
+  type: T,
+): Promise<Array<PersistentEventBase<RoomVersion, T>>> {
+  const state = await this.getFullRoomState(roomId);
+  const events: Array<PersistentEventBase<RoomVersion, T>> = [];
+  for (const [, event] of state) {
+    if (event.type === type) {
+      events.push(event as PersistentEventBase<RoomVersion, T>);
+    }
+  }
+  return events;
+}

859-861: Good: enforce non‑empty member list

Throwing when no members are found aligns with previous feedback to not “allow” empty membership sets.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab312a and 65aa4d8.

📒 Files selected for processing (1)
  • packages/federation-sdk/src/services/state.service.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/state.service.ts (3)
packages/federation-sdk/src/index.ts (1)
  • State (23-23)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/types/v3-11.ts (1)
  • PduType (45-45)
🔇 Additional comments (1)
packages/federation-sdk/src/services/state.service.ts (1)

26-26: Rename exported State to ResolvedState to avoid cross-package type collisions

@hs/room already exports State = Map<StateMapKey, EventID>. Exporting another State here as Map<StateMapKey, PersistentEventBase> is a confusing, breaking public API — rename this export (e.g., ResolvedState) and keep an internal alias to avoid churn.

File: packages/federation-sdk/src/services/state.service.ts:26

Apply this diff locally and then re-export ResolvedState from the SDK index:

-export type State = Map<StateMapKey, PersistentEventBase>;
+// Avoid cross-package type-name collision with @hs/room's exported `State`
+export type ResolvedState = Map<StateMapKey, PersistentEventBase>;
+type State = ResolvedState;

Run to find potential collisions and spots to update imports:

rg -nP -C2 "import\s*\{\s*State\s*\}\s*from\s*['\"]@hs/(room|federation-sdk)['\"]"

Comment on lines +862 to +924
return members.map((member) => {
const server = member.split(':').pop();
if (!server) {
throw new Error(`Invalid member format of room ${roomId}: ${member}`);
}
return server;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: incorrect server parsing for ports/IPv6; dedupe servers

member.split(':').pop() returns only the last segment, breaking for servers with ports (example.org:8448) or IPv6 ([2001:db8::1]:8448). Parse everything after the first colon and return unique servers.

Apply this diff:

-  return members.map((member) => {
-    const server = member.split(':').pop();
-    if (!server) {
-      throw new Error(`Invalid member format of room ${roomId}: ${member}`);
-    }
-    return server;
-  });
+  const servers = new Set<string>();
+  for (const member of members) {
+    const idx = member.indexOf(':');
+    if (idx < 0 || idx === member.length - 1) {
+      throw new Error(`Invalid member format in room ${roomId}: ${member}`);
+    }
+    // Everything after the first ":" is the server name; it may contain ":" (port/IPv6).
+    const server = member.slice(idx + 1);
+    servers.add(server);
+  }
+  return [...servers];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return members.map((member) => {
const server = member.split(':').pop();
if (!server) {
throw new Error(`Invalid member format of room ${roomId}: ${member}`);
}
return server;
});
const servers = new Set<string>();
for (const member of members) {
const idx = member.indexOf(':');
if (idx < 0 || idx === member.length - 1) {
throw new Error(`Invalid member format in room ${roomId}: ${member}`);
}
// Everything after the first ":" is the server name; it may contain ":" (port/IPv6).
const server = member.slice(idx + 1);
servers.add(server);
}
return [...servers];

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/homeserver/src/middlewares/acl.middleware.ts (1)

13-14: Include query string in the signed URI (spec compliance).

Matrix signs path + query; using only pathname will cause signature mismatches on requests with query params.

-		const uri = new URL(request.url).pathname;
+		const url = new URL(request.url);
+		const uri = `${url.pathname}${url.search}`;
🧹 Nitpick comments (1)
packages/homeserver/src/middlewares/acl.middleware.ts (1)

9-12: Read Authorization header via Fetch API and normalize method.

More robust across runtimes and avoids case/key pitfalls. Normalizing method is harmless and defensive.

-		const { params, headers, request, set } = req;
+		const { params, request, set } = req;
@@
-		const authorizationHeader = headers.authorization || '';
-		const method = request.method;
+		const authorizationHeader = request.headers.get('authorization') ?? '';
+		const method = request.method.toUpperCase();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65aa4d8 and aa8b95f.

📒 Files selected for processing (2)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (3 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/homeserver/src/middlewares/acl.middleware.ts (3)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (184-231)
packages/federation-sdk/src/index.ts (2)
  • EventAuthorizationService (40-40)
  • errCodes (69-69)
packages/federation-sdk/src/utils/response-codes.ts (1)
  • errCodes (1-20)

Comment on lines 15 to 30
const result = await federationAuth.canAccessEventFromAuthorizationHeader(
eventId,
authorizationHeader,
method,
uri,
);

if (!result.authorized) {
set.status = errCodes[result.errorCode].status;
return {
errcode: errCodes[result.errorCode].errcode,
error: errCodes[result.errorCode].error,
};
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Harden error handling and unknown error codes.

Guard against service exceptions and unknown codes to always return a spec’d body.

-		const result = await federationAuth.canAccessEventFromAuthorizationHeader(
-			eventId,
-			authorizationHeader,
-			method,
-			uri,
-		);
+		let result;
+		try {
+			result = await federationAuth.canAccessEventFromAuthorizationHeader(
+				eventId,
+				authorizationHeader,
+				method,
+				uri,
+			);
+		} catch {
+			set.status = errCodes.M_UNKNOWN.status;
+			return {
+				errcode: errCodes.M_UNKNOWN.errcode,
+				error: errCodes.M_UNKNOWN.error,
+			};
+		}
@@
-		if (!result.authorized) {
-			set.status = errCodes[result.errorCode].status;
-			return {
-				errcode: errCodes[result.errorCode].errcode,
-				error: errCodes[result.errorCode].error,
-			};
-		}
+		if (!result.authorized) {
+			const code = errCodes[result.errorCode] ?? errCodes.M_UNKNOWN;
+			set.status = code.status;
+			return { errcode: code.errcode, error: code.error };
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await federationAuth.canAccessEventFromAuthorizationHeader(
eventId,
authorizationHeader,
method,
uri,
);
if (!result.authorized) {
set.status = errCodes[result.errorCode].status;
return {
errcode: errCodes[result.errorCode].errcode,
error: errCodes[result.errorCode].error,
};
}
});
let result;
try {
result = await federationAuth.canAccessEventFromAuthorizationHeader(
eventId,
authorizationHeader,
method,
uri,
);
} catch {
set.status = errCodes.M_UNKNOWN.status;
return {
errcode: errCodes.M_UNKNOWN.errcode,
error: errCodes.M_UNKNOWN.error,
};
}
if (!result.authorized) {
const code = errCodes[result.errorCode] ?? errCodes.M_UNKNOWN;
set.status = code.status;
return { errcode: code.errcode, error: code.error };
}
});
🤖 Prompt for AI Agents
In packages/homeserver/src/middlewares/acl.middleware.ts around lines 15 to 29,
the call to federationAuth.canAccessEventFromAuthorizationHeader is not guarded
and the handler assumes result.errorCode always maps to errCodes; wrap the await
call in a try/catch to convert any thrown exceptions into a known error
response, and when result.authorized is false, defensively resolve
result.errorCode against errCodes with a safe fallback (e.g., a default INTERNAL
or FORBIDDEN mapping) so set.status is always assigned and the returned object
always contains errcode and error fields from the fallback when the code is
unknown; ensure no exceptions escape and the response body conforms to the spec
in all failure cases.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/homeserver/src/controllers/internal/room.controller.ts (1)

319-324: Restore DTO validation and structured error handling in ban route

This handler no longer validates inputs or returns typed errors (unlike other endpoints here). Add back params/body validation and 400/500 handling to avoid 500s on bad input.

Apply this diff at the start of the handler:

-			async ({
-				params,
-				body,
-			}): Promise<InternalBanUserResponse | ErrorResponse> => {
+			async ({
+				params,
+				body,
+				set,
+			}): Promise<InternalBanUserResponse | ErrorResponse> => {
+				const roomIdParse = RoomIdDto.safeParse(params.roomId);
+				const userIdParse = UsernameDto.safeParse(params.userIdToBan);
+				const bodyParse = InternalBanUserBodyDto.safeParse(body);
+				if (!roomIdParse.success || !userIdParse.success || !bodyParse.success) {
+					set.status = 400;
+					return {
+						error: 'Invalid request',
+						details: {
+							roomId: roomIdParse.error?.flatten(),
+							userId: userIdParse.error?.flatten(),
+							body: bodyParse.error?.flatten(),
+						},
+					};
+				}
+				const { senderUserId /*, reason, targetServers */ } = bodyParse.data;
packages/room/src/authorizartion-rules/rules.ts (1)

60-76: Centralize recognized room versions; include '12' (spec updated Aug 11, 2025)

Replace the inline ['1'…'11'] with a single exported Set/constant (or import) and add '12' — as of 2025-09-15 Matrix recognizes room versions 1–12. Location: packages/room/src/authorizartion-rules/rules.ts lines 60–76.

♻️ Duplicate comments (5)
packages/federation-sdk/src/services/event-authorization.service.ts (4)

184-211: Use host-only for ACL and membership checks (strip :port)
Using serverName with port causes false decisions for ACL/membership. Compare on host-only.

-	private async canAccessEvent(
+	private async canAccessEvent(
 		eventId: string,
 		serverName: string,
 	): Promise<boolean> {
@@
-			const roomId = event.event.room_id;
+			const roomId = event.event.room_id;
+			const originHost = this.stripPort(serverName);
@@
-			const isServerAllowed = await this.checkServerAcl(roomId, serverName);
+			const isServerAllowed = await this.checkServerAcl(roomId, originHost);
@@
-			const serversInRoom = await this.stateService.getServersInRoom(roomId);
-			if (serversInRoom.includes(serverName)) {
+			const serversInRoom = await this.stateService.getServersInRoom(roomId);
+			if (serversInRoom.includes(originHost)) {
 				this.logger.debug(`Server ${serverName} is in room, allowing access`);
 				return true;
 			}

Add helper inside the class:

private stripPort(serverName: string): string {
	if (serverName.startsWith('[')) {
		const m = serverName.match(/^\[([^\]]+)\](?::\d+)?$/);
		return m ? `[${m[1]}]` : serverName;
	}
	return serverName.replace(/:\d+$/, '');
}

284-287: Do not log full Authorization header

Redact the header to avoid credential leakage.

-			this.logger.error(
-				{ error, eventId, authorizationHeader, method, uri, body },
-				'Error checking event access',
-			);
+			this.logger.error(
+				{ error, eventId, method, uri, body, authorizationHeader: '<redacted>' },
+				'Error checking event access',
+			);

295-350: Fix ACL semantics and host handling per spec

  • Missing allow should mean allow-all (subject to deny and allow_ip_literals).
  • Evaluate patterns and IP-literal checks on host-only (no port).
-		const serverAclContent = aclEvent.getContent();
-		const {
-			allow = [],
-			deny = [],
-			allow_ip_literals = true,
-		} = serverAclContent;
+		const serverAclContent = aclEvent.getContent() as {
+			allow?: string[];
+			deny?: string[];
+			allow_ip_literals?: boolean;
+		};
+		const allow = Array.isArray(serverAclContent.allow)
+			? serverAclContent.allow
+			: undefined;
+		const deny = Array.isArray(serverAclContent.deny) ? serverAclContent.deny : [];
+		const allow_ip_literals = serverAclContent.allow_ip_literals ?? true;
+
+		const hostOnly = this.stripPort(serverName);
 
-		const isIpLiteral =
-			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) ||
-			/^\[.*\](:\d+)?$/.test(serverName); // IPv6
+		const isIpLiteral =
+			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOnly) ||
+			/^\[[^\]]+\]$/.test(hostOnly); // IPv6 literal in brackets
 		if (isIpLiteral && !allow_ip_literals) {
 			this.logger.debug(`Server ${serverName} denied: IP literals not allowed`);
 			return false;
 		}
 
 		for (const pattern of deny) {
-			if (this.matchesServerPattern(serverName, pattern)) {
+			if (this.matchesServerPattern(hostOnly, pattern)) {
 				this.logger.debug(
 					`Server ${serverName} matches deny pattern: ${pattern}`,
 				);
 				return false;
 			}
 		}
 
-		// if allow list is empty, deny all servers (as per Matrix spec)
-		// empty allow list means no servers are allowed
-		if (allow.length === 0) {
-			this.logger.debug(`Server ${serverName} denied: allow list is empty`);
-			return false;
-		}
+		// Explicit empty allow => deny all; missing allow => allow-all (subject to deny)
+		if (allow !== undefined) {
+			if (allow.length === 0) {
+				this.logger.debug(`Server ${serverName} denied: allow list is explicitly empty`);
+				return false;
+			}
+			for (const pattern of allow) {
+				if (this.matchesServerPattern(hostOnly, pattern)) {
+					this.logger.debug(
+						`Server ${serverName} matches allow pattern: ${pattern}`,
+					);
+					return true;
+				}
+			}
+			this.logger.debug(`Server ${serverName} not in explicit allow list`);
+			return false;
+		}
 
-		for (const pattern of allow) {
-			if (this.matchesServerPattern(serverName, pattern)) {
-				this.logger.debug(
-					`Server ${serverName} matches allow pattern: ${pattern}`,
-				);
-				return true;
-			}
-		}
-
-		this.logger.debug(`Server ${serverName} not in allow list`);
-		return false;
+		return true;

352-371: Avoid regex-from-input; implement simple '*' wildcard matcher

Building regex from untrusted ACL patterns risks ReDoS and goes beyond spec needs.

-	private matchesServerPattern(serverName: string, pattern: string): boolean {
-		if (serverName === pattern) {
-			return true;
-		}
-
-		let regexPattern = pattern
-			.replace(/[.+^${}()|[\]\\]/g, '\\$&')
-			.replace(/\*/g, '.*')
-			.replace(/\?/g, '.');
-
-		regexPattern = `^${regexPattern}$`;
-
-		try {
-			const regex = new RegExp(regexPattern);
-			return regex.test(serverName);
-		} catch (error) {
-			this.logger.warn(`Invalid ACL pattern: ${pattern}`, error);
-			return false;
-		}
-	}
+	private matchesServerPattern(serverName: string, pattern: string): boolean {
+		if (pattern === '*' || serverName === pattern) return true;
+		// Support '*' only; treat all other chars literally
+		const parts = pattern.split('*');
+		let pos = 0;
+		if (!pattern.startsWith('*')) {
+			if (!serverName.startsWith(parts[0])) return false;
+			pos = parts[0].length;
+			parts.shift();
+		}
+		for (let i = 0; i < parts.length - 1; i++) {
+			const seg = parts[i];
+			if (!seg) continue;
+			const idx = serverName.indexOf(seg, pos);
+			if (idx === -1) return false;
+			pos = idx + seg.length;
+		}
+		const last = parts[parts.length - 1] ?? '';
+		return pattern.endsWith('*') ? true : serverName.endsWith(last);
+	}
packages/federation-sdk/src/services/state.service.ts (1)

858-869: Server parsing breaks for ports/IPv6; also dedupe results

Split on the first colon after localpart and keep everything after it; return unique servers.

-		const members = await this.getMembersOfRoom(roomId);
-		if (!members.length) {
-			throw new Error(`No members found in room ${roomId}`);
-		}
-		return members.map((member) => {
-			const server = member.split(':').pop();
-			if (!server) {
-				throw new Error(`Invalid member format of room ${roomId}: ${member}`);
-			}
-			return server;
-		});
+		const members = await this.getMembersOfRoom(roomId);
+		if (members.length === 0) {
+			throw new Error(`No members found in room ${roomId}`);
+		}
+		const servers = new Set<string>();
+		for (const member of members) {
+			const idx = member.indexOf(':'); // '@localpart:server'
+			if (idx < 0 || idx === member.length - 1) {
+				throw new Error(`Invalid member format in room ${roomId}: ${member}`);
+			}
+			const server = member.slice(idx + 1); // may include brackets for IPv6; no port in user_id
+			servers.add(server);
+		}
+		return [...servers];
🧹 Nitpick comments (10)
packages/federation-sdk/src/services/send-join.service.ts (1)

73-83: Guard membership when switching to untyped getContent()

Avoid emitting undefined membership; validate once and reuse content locals.

 		this.emitterService.emit('homeserver.matrix.accept-invite', {
 			event_id: eventId,
 			room_id: roomId,
 			sender: signedJoinEvent.sender,
 			origin_server_ts: signedJoinEvent.originServerTs,
-			content: {
-				avatar_url: signedJoinEvent.getContent().avatar_url || null,
-				displayname: signedJoinEvent.getContent().displayname || '',
-				membership: signedJoinEvent.getContent().membership,
-			},
+			content: {
+				// validate content once
+				// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+				...((
+					content => {
+						if (typeof content?.membership !== 'string') {
+							throw new Error('Invalid m.room.member content: missing membership');
+						}
+						return {
+							avatar_url: content.avatar_url ?? null,
+							displayname: content.displayname ?? '',
+							membership: content.membership,
+						};
+					}
+				)(signedJoinEvent.getContent())),
+			},
 		});
packages/federation-sdk/src/services/state.service.ts (2)

26-26: Exported type name collides with @hs/room’s State

Both packages export State with different shapes (EventID vs PersistentEventBase). This will confuse consumers.

Rename this to ResolvedState or StateMapResolved and re-export accordingly to avoid ambiguous imports.


289-301: Type the accumulator to avoid any[] leakage

Give events a concrete type for safer inference.

-		const events = [];
+		const events: PersistentEventBase<RoomVersion, P>[] = [];
packages/room/src/state_resolution/definitions/definitions.ts (1)

26-27: Avoid double getMembership() call

Cache the value once; tiny win and clearer intent.

-		(event.isMembershipEvent() &&
-			(event.getMembership() === 'leave' || event.getMembership() === 'ban') &&
+		(event.isMembershipEvent() &&
+			((() => { const m = event.getMembership(); return m === 'leave' || m === 'ban'; })()) &&
 			event.sender !== event.stateKey)
packages/homeserver/src/controllers/internal/room.controller.ts (1)

397-401: Handle rejected auth before returning

Mirror invite flow: if the event was rejected by auth rules, surface that instead of returning 200.

Apply this diff:

-				await stateService.persistStateEvent(membershipEvent);
-
-				return {
-					eventId: membershipEvent.eventId,
-				};
+				await stateService.persistStateEvent(membershipEvent);
+				if (membershipEvent.rejected) {
+					set.status = 403;
+					return {
+						error: membershipEvent.rejectedReason ?? 'Ban rejected by auth rules',
+						details: {},
+					};
+				}
+				return { eventId: membershipEvent.eventId };
packages/room/src/manager/power-level-event-wrapper.ts (1)

24-29: toEventBase can be undefined; fix the return type and narrow before casting

Current cast can return undefined at runtime while the signature claims non-null. Return | undefined and gate on type to avoid unsoundness.

Apply this diff:

-	toEventBase(): PersistentEventBase<RoomVersion, 'm.room.power_levels'> {
-		return this.event as PersistentEventBase<
-			RoomVersion,
-			'm.room.power_levels'
-		>;
-	}
+	toEventBase():
+		| PersistentEventBase<RoomVersion, 'm.room.power_levels'>
+		| undefined {
+		return this.event?.isPowerLevelEvent()
+			? (this.event as PersistentEventBase<RoomVersion, 'm.room.power_levels'>)
+			: undefined;
+	}
packages/room/src/authorizartion-rules/rules.ts (1)

172-223: Prefer StateResolverAuthorizationError over plain Error for consistency

Several branches throw Error(...) instead of StateResolverAuthorizationError, making error handling uneven.

Example:

-			if (sender !== invitee) {
-				throw new Error('state_key does not match the sender');
-			}
+			if (sender !== invitee) {
+				throw new StateResolverAuthorizationError(
+					'state_key does not match the sender',
+					{ eventFailed: membershipEventToCheck }
+				);
+			}

Apply similarly in other branches that perform auth decisions.

packages/room/src/manager/event-wrapper.ts (3)

154-157: Nice: explicit, typed event-kind guards

These predicates improve flow typing across the module. Consider adding guards for ACL and history visibility to align with this PR’s objectives.

Apply this diff to add two more guards:

   isAliasEvent(): this is PersistentEventBase<T, 'm.room.aliases'> {
     return this.isState() && this.type === 'm.room.aliases';
   }
+
+  isServerAclEvent(): this is PersistentEventBase<T, 'm.room.server_acl'> {
+    return this.isState() && this.type === 'm.room.server_acl';
+  }
+
+  isHistoryVisibilityEvent(): this is PersistentEventBase<
+    T,
+    'm.room.history_visibility'
+  > {
+    return this.isState() && this.type === 'm.room.history_visibility';
+  }

Also applies to: 158-160, 162-164, 166-168, 170-172, 174-176, 178-183, 185-187


189-194: Improve error context and return typing for getMembership

Include the actual event type in the error to ease debugging, and surface the precise return type.

Apply this diff:

-	getMembership() {
-		if (!this.isMembershipEvent())
-			throw new Error('Event is not a membership event');
-
-		return (this.getContent() as PduMembershipEventContent).membership;
-	}
+	getMembership(): PduMembershipEventContent['membership'] {
+		if (!this.isMembershipEvent()) {
+			throw new Error(`Event is not a membership event; got type: ${this.type}`);
+		}
+		return (this.getContent() as PduMembershipEventContent).membership;
+	}

196-202: Mirror the membership improvements for getJoinRule

Add precise return type and richer error message.

Apply this diff:

-	getJoinRule() {
-		if (!this.isJoinRuleEvent()) {
-			throw new Error('Event is not a join rule event');
-		}
-
-		return (this.getContent() as PduJoinRuleEventContent).join_rule;
-	}
+	getJoinRule(): PduJoinRuleEventContent['join_rule'] {
+		if (!this.isJoinRuleEvent()) {
+			throw new Error(`Event is not a join rule event; got type: ${this.type}`);
+		}
+		return (this.getContent() as PduJoinRuleEventContent).join_rule;
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa8b95f and 6a97e1d.

📒 Files selected for processing (11)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/send-join.service.ts (1 hunks)
  • packages/federation-sdk/src/services/state.service.ts (6 hunks)
  • packages/homeserver/src/controllers/internal/invite.controller.ts (2 hunks)
  • packages/homeserver/src/controllers/internal/room.controller.ts (2 hunks)
  • packages/room/src/authorizartion-rules/rules.ts (7 hunks)
  • packages/room/src/manager/event-wrapper.ts (2 hunks)
  • packages/room/src/manager/power-level-event-wrapper.ts (2 hunks)
  • packages/room/src/manager/room-state.ts (5 hunks)
  • packages/room/src/state_resolution/definitions/definitions.ts (1 hunks)
  • packages/room/src/types/v3-11.ts (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.421Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.421Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/room/src/types/v3-11.ts
  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (7)
packages/room/src/manager/power-level-event-wrapper.ts (1)
packages/room/src/manager/type.ts (1)
  • RoomVersion (14-14)
packages/room/src/authorizartion-rules/rules.ts (3)
packages/room/src/authorizartion-rules/errors.ts (1)
  • StateResolverAuthorizationError (24-24)
packages/room/src/manager/event-wrapper.ts (1)
  • event (102-112)
packages/core/src/events/m.room.create.ts (1)
  • roomCreateEvent (26-43)
packages/room/src/manager/room-state.ts (1)
packages/room/src/state_resolution/definitions/definitions.ts (1)
  • getStateMapKey (10-15)
packages/federation-sdk/src/services/state.service.ts (2)
packages/federation-sdk/src/index.ts (1)
  • State (23-23)
packages/room/src/types/_common.ts (2)
  • State (9-9)
  • StateMapKey (7-7)
packages/room/src/types/v3-11.ts (1)
packages/room/src/types/_common.ts (1)
  • PduForType (11-11)
packages/room/src/manager/event-wrapper.ts (1)
packages/room/src/types/v3-11.ts (3)
  • PduContent (731-731)
  • PduMembershipEventContent (114-116)
  • PduJoinRuleEventContent (186-188)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
packages/federation-sdk/src/services/config.service.ts (2)
  • ConfigService (76-153)
  • serverName (94-96)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 364-364: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (25)
packages/federation-sdk/src/services/state.service.ts (3)

402-422: LGTM: generic signEvent improves type inference

Good change; preserves E and returns same instance type.


429-431: LGTM: room_version via untyped content is consistent with the repo-wide shift

No issues.


545-547: LGTM: untyped getContent for create events

Consistent with newer content access.

packages/room/src/types/v3-11.ts (4)

229-247: ACL content schema looks correct (allow/deny/allow_ip_literals with default=true)

Good addition; aligns with spec expectations.


655-659: Wiring m.room.server_acl into PDU union is correct

Empty state_key variant matches spec.


718-719: PDU union includes server ACL

All good.


731-731: PduContent becomes generic — confirm downstream typing

This should be backward-compatible via the default parameter, but please ensure external imports don’t rely on the old non-generic form in ways that now narrow unexpectedly.

packages/federation-sdk/src/services/event-authorization.service.ts (1)

114-182: Signature verification flow: LGTM (happy-path and key lookup/cache)

Clear separation of concerns; good early returns for invalid cases.

packages/homeserver/src/controllers/internal/room.controller.ts (2)

368-371: Good: stronger create-event guard

Validating both presence and type with isCreateEvent() prevents mis-typed state reads.


385-386: OK to use untyped getContent() here

Given the prior type guard, accessing .room_version via untyped content is safe.

packages/homeserver/src/controllers/internal/invite.controller.ts (2)

35-37: Good: runtime type guard for create event

Prevents invalid content access when state is corrupt or mismatched.


51-52: OK to use untyped getContent()

Safe given the guard. Keeps the code consistent with other modules.

packages/room/src/manager/room-state.ts (6)

25-30: Good: validate create event shape

Ensures we only read creator from a true m.room.create.


38-43: Good: membership accessor guards

Returning undefined when no valid membership event exists is appropriate.


83-88: Good default for privacy

Falling back to 'public' if join rules missing or wrong type matches expected defaults.


107-112: Topic: safe fallback

Returning empty string for missing/invalid topic is consistent and non-throwing.


137-142: Power levels: tolerant access

Returning undefined when the event is missing or mistyped avoids crashes downstream.


148-153: Good: validate create event for version extraction

Prevents invalid .room_version reads.

packages/room/src/authorizartion-rules/rules.ts (7)

35-39: Good: require isCreateEvent() in create checks

Avoids accepting malformed “create” events.


121-122: Membership checks: correct to require a real membership event

This avoids treating other state as membership.


168-169: Content access via untyped getContent() is fine

You guarded type above; content read is safe.


188-191: Join path: correct creator short‑circuit

Accepting creator’s initial join when the only prev is create matches the spec intent.


711-715: Good: assert the room create event’s type, not only presence

Prevents reading create content from a wrong event.


719-721: OK: m.federate check via untyped content

Guarded by the type check above; this is correct and simple.


522-524: Optional chaining here pairs well with the proposed toEventBase() fix

Once toEventBase() returns | undefined, this access remains safe.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
packages/federation-sdk/src/services/event-authorization.service.ts (4)

184-211: ACL/membership must ignore port; operate on host‑only to avoid false auth decisions.
Using serverName with :port causes mismatches vs room membership and ACL patterns.

Apply this diff:

@@
-	private async canAccessEvent(
+	private async canAccessEvent(
 		eventId: string,
 		serverName: string,
 	): Promise<boolean> {
@@
-			const roomId = event.event.room_id;
+			const roomId = event.event.room_id;
+			const originHost = this.stripPort(serverName);
@@
-			const isServerAllowed = await this.checkServerAcl(roomId, serverName);
+			const isServerAllowed = await this.checkServerAcl(roomId, originHost);
@@
-			const serversInRoom = await this.stateService.getServersInRoom(roomId);
-			if (serversInRoom.includes(serverName)) {
+			const serversInRoom = await this.stateService.getServersInRoom(roomId);
+			if (serversInRoom.includes(originHost)) {
 				this.logger.debug(`Server ${serverName} is in room, allowing access`);
 				return true;
 			}

Add the helper inside this class:

private stripPort(serverName: string): string {
	// [IPv6]:port -> [IPv6]
	if (serverName.startsWith('[')) {
		const m = serverName.match(/^\[([^\]]+)\](?::\d+)?$/);
		return m ? `[${m[1]}]` : serverName;
	}
	// host:port -> host
	return serverName.replace(/:\d+$/, '');
}

295-350: Fix ACL semantics per spec; also normalize to host‑only and avoid IP‑literal false negatives.

  • Missing allow must mean “allow all” (subject to deny/allow_ip_literals), not deny‑all.
  • Evaluate ACL against host‑only (no port).
  • Check IP‑literals on host‑only.

Apply:

@@
-		const serverAclContent = aclEvent.getContent();
-		const {
-			allow = [],
-			deny = [],
-			allow_ip_literals = true,
-		} = serverAclContent;
+		const serverAclContent = aclEvent.getContent() as {
+			allow?: string[];
+			deny?: string[];
+			allow_ip_literals?: boolean;
+		};
+		// Preserve presence/absence to distinguish "missing" vs "present but empty"
+		const allow = Array.isArray(serverAclContent.allow)
+			? serverAclContent.allow
+			: undefined;
+		const deny = Array.isArray(serverAclContent.deny)
+			? serverAclContent.deny
+			: [];
+		const allow_ip_literals = serverAclContent.allow_ip_literals ?? true;
+		const hostOnly = this.stripPort(serverName);
@@
-		const isIpLiteral =
-			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) ||
-			/^\[.*\](:\d+)?$/.test(serverName); // IPv6
+		const isIpLiteral =
+			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOnly) ||
+			/^\[[^\]]+\]$/.test(hostOnly); // IPv6
@@
-		for (const pattern of deny) {
-			if (this.matchesServerPattern(serverName, pattern)) {
+		for (const pattern of deny) {
+			if (this.matchesServerPattern(hostOnly, pattern)) {
 				this.logger.debug(
 					`Server ${serverName} matches deny pattern: ${pattern}`,
 				);
 				return false;
 			}
 		}
@@
-		// if allow list is empty, deny all servers (as per Matrix spec)
-		// empty allow list means no servers are allowed
-		if (allow.length === 0) {
-			this.logger.debug(`Server ${serverName} denied: allow list is empty`);
-			return false;
-		}
+		// If 'allow' is present and empty, deny all. If 'allow' is missing, allow-all (subject to deny rules).
+		if (allow !== undefined && allow.length === 0) {
+			this.logger.debug(
+				`Server ${serverName} denied: allow list is explicitly empty`,
+			);
+			return false;
+		}
@@
-		for (const pattern of allow) {
-			if (this.matchesServerPattern(serverName, pattern)) {
-				this.logger.debug(
-					`Server ${serverName} matches allow pattern: ${pattern}`,
-				);
-				return true;
-			}
-		}
-
-		this.logger.debug(`Server ${serverName} not in allow list`);
-		return false;
+		if (allow !== undefined) {
+			for (const pattern of allow) {
+				if (this.matchesServerPattern(hostOnly, pattern)) {
+					this.logger.debug(
+						`Server ${serverName} matches allow pattern: ${pattern}`,
+					);
+					return true;
+				}
+			}
+			this.logger.debug(`Server ${serverName} not in explicit allow list`);
+			return false;
+		}
+		// No explicit allow list -> allowed (not denied above)
+		return true;

352-371: Do not build RegExp from untrusted ACL patterns; use simple '*' wildcard matching.
Removes ReDoS risk and matches spec (only '*' supported).

Apply:

 	private matchesServerPattern(serverName: string, pattern: string): boolean {
-		if (serverName === pattern) {
-			return true;
-		}
-
-		let regexPattern = pattern
-			.replace(/[.+^${}()|[\]\\]/g, '\\$&')
-			.replace(/\*/g, '.*')
-			.replace(/\?/g, '.');
-
-		regexPattern = `^${regexPattern}$`;
-
-		try {
-			const regex = new RegExp(regexPattern);
-			return regex.test(serverName);
-		} catch (error) {
-			this.logger.warn(`Invalid ACL pattern: ${pattern}`, error);
-			return false;
-		}
+		// Fast paths
+		if (pattern === '*' || serverName === pattern) return true;
+		// Greedy linear '*' matcher
+		let i = 0; // serverName index
+		let j = 0; // pattern index
+		let star = -1;
+		let match = 0;
+		while (i < serverName.length) {
+			if (j < pattern.length && (pattern[j] === serverName[i])) {
+				i++; j++;
+			} else if (j < pattern.length && pattern[j] === '*') {
+				star = j++;
+				match = i;
+			} else if (star !== -1) {
+				j = star + 1;
+				i = ++match;
+			} else {
+				return false;
+			}
+		}
+		while (j < pattern.length && pattern[j] === '*') j++;
+		return j === pattern.length;
 	}

284-287: Redact Authorization header in error logs.
Avoid leaking credentials/tokens.

Apply:

-			this.logger.error(
-				{ error, eventId, authorizationHeader, method, uri, body },
-				'Error checking event access',
-			);
+			this.logger.error(
+				{ error, eventId, method, uri, body, authorizationHeader: '<redacted>' },
+				'Error checking event access',
+			);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a97e1d and 0479240.

📒 Files selected for processing (7)
  • packages/federation-sdk/src/container.ts (1 hunks)
  • packages/federation-sdk/src/index.ts (4 hunks)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (3 hunks)
  • packages/federation-sdk/src/utils/response-codes.ts (1 hunks)
  • packages/homeserver/src/controllers/federation/transactions.controller.ts (3 hunks)
  • packages/homeserver/src/middlewares/acl.middleware.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/federation-sdk/src/utils/response-codes.ts
  • packages/federation-sdk/src/container.ts
  • packages/homeserver/src/middlewares/acl.middleware.ts
  • packages/federation-sdk/src/index.ts
  • packages/federation-sdk/src/services/state.service.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.421Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.421Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (2)
packages/homeserver/src/controllers/federation/transactions.controller.ts (2)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
  • canAccessEvent (184-231)
packages/homeserver/src/middlewares/acl.middleware.ts (1)
  • canAccessEvent (5-30)
packages/federation-sdk/src/services/event-authorization.service.ts (3)
packages/federation-sdk/src/services/state.service.ts (1)
  • singleton (35-931)
packages/federation-sdk/src/services/event.service.ts (1)
  • singleton (44-808)
packages/federation-sdk/src/services/config.service.ts (2)
  • ConfigService (76-153)
  • serverName (94-96)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 364-364: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (3)
packages/homeserver/src/controllers/federation/transactions.controller.ts (3)

1-5: DI import for EventAuthorizationService looks correct.
Wires the SDK service into the controller cleanly.


16-16: Middleware import OK.
Interface matches the usage below.


21-21: Resolve once at plugin init (good).
Singleton resolve at load-time avoids per‑request overhead.

};
},
{
use: canAccessEvent(eventAuthService),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify Elysia supports per‑route use: in this project’s version.
If not supported, the middleware won’t run. Confirm the Elysia version or switch to a route‑scoped beforeHandle or a .group(..., { use: plugin }) wrapper for this GET only.


🌐 Web query:

Does Elysia support a `use` property in a single route’s options (to attach a plugin only to that route)? Cite docs and the minimum version that supports it.

💡 Result:

Short answer: No — Elysia does not provide a per-route "use" option to attach a plugin to a single route. The official route documentation describes route options for things like local schema/hooks but does not list a "use" property, and the plugin docs describe attaching plugins via app.use (instance‑level) or using guards/plugins as instances, not a per‑route options "use". [1][2]

Sources:

  • Route docs (shows route signature and local schema/hooks in route options; no "use" per-route option). [1]
  • Plugin docs (describes .use to register plugins on an instance and plugin patterns). [2]

Remove per‑route use — Elysia does not support use in route options.

Confirmed: route options expose a route-level beforeHandle for auth/validation; plugins are registered via .use on an instance and scoping is done with group/guard. (elysiajs.com)

File: packages/homeserver/src/controllers/federation/transactions.controller.ts (line ~68) — replace

  • use: canAccessEvent(eventAuthService),
    with either:
  • beforeHandle: canAccessEvent(eventAuthService) (preferred for per-route auth), or
  • wrap the route in a group/guard and call app.use(...) inside that scope to apply the plugin only to that group.
🤖 Prompt for AI Agents
In packages/homeserver/src/controllers/federation/transactions.controller.ts
around line 68, the route option incorrectly uses "use:
canAccessEvent(eventAuthService)" which Elysia does not support; replace that
option with "beforeHandle: canAccessEvent(eventAuthService)" to apply per-route
auth, or alternatively remove the per-route plugin and scope the plugin via
app.group/guard and app.use(...) to apply canAccessEvent to the route group.

roomId: string,
serverName: string,
): Promise<boolean> {
const [aclEvent] = await this.stateService.getStateEventsByType(
Copy link
Member

Choose a reason for hiding this comment

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

is it possible to have more than one state of type m.room.server_acl? if so this code is just considering the first one, is that ok?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (6)
packages/room/src/manager/event-wrapper.ts (2)

121-123: Remove generic from getContent to prevent unsafe downcasts.

Returning PduContent<Type> avoids caller-supplied generics that bypass narrowing. See prior review; repeating for visibility.

Apply:

-	getContent<T extends PduContent<Type>>(): T {
-		return this.rawEvent.content as T;
-	}
+	getContent(): PduContent<Type> {
+		return this.rawEvent.content as PduContent<Type>;
+	}

371-383: Update membership-content call sites after getContent change.

Stop using getContent<...>(); narrow locally. Reiterating past comment so the new guards can be leveraged here.

Apply:

-				this.getContent<PduMembershipEventContent>().third_party_invite
+				(this.getContent() as PduMembershipEventContent).third_party_invite
-				this.getContent<PduMembershipEventContent>()
-					.join_authorised_via_users_server
+				(this.getContent() as PduMembershipEventContent)
+					.join_authorised_via_users_server
packages/federation-sdk/src/services/event-authorization.service.ts (4)

195-211: Normalize to host-only (no :port) for membership/ACL checks.

Using serverName with :port causes false negatives when comparing against room servers and ACL patterns.

Apply:

 			const roomId = event.event.room_id;
-			const state = await this.stateService.getFullRoomState(roomId);
+			const state = await this.stateService.getFullRoomState(roomId);
+			const originHost = this.stripPort(serverName);
@@
-			const isServerAllowed = await this.checkServerAcl(aclEvent, serverName);
+			const isServerAllowed = await this.checkServerAcl(aclEvent, originHost);
@@
-			if (serversInRoom.includes(serverName)) {
+			if (serversInRoom.includes(originHost)) {
 				this.logger.debug(`Server ${serverName} is in room, allowing access`);
 				return true;
 			}

Add inside the class:

private stripPort(serverName: string): string {
	// [IPv6]:port -> [IPv6]
	if (serverName.startsWith('[')) {
		const m = serverName.match(/^\[([^\]]+)\](?::\d+)?$/);
		return m ? `[${m[1]}]` : serverName;
	}
	// host:port -> host
	return serverName.replace(/:\d+$/, '');
}

277-280: Do not log raw Authorization header. Redact it.

Avoid credential leakage in logs.

Apply:

-			this.logger.error(
-				{ error, eventId, authorizationHeader, method, uri, body },
-				'Error checking event access',
-			);
+			this.logger.error(
+				{ error, eventId, method, uri, body, authorizationHeader: '<redacted>' },
+				'Error checking event access',
+			);

288-339: Fix ACL semantics and IP/host handling to match Matrix spec.

  • Missing allow must mean “allow all” (subject to deny and allow_ip_literals), not deny-all.
  • Operate on host-only for pattern and IP-literal checks.

Apply:

-		const serverAclContent = aclEvent.getContent();
-		const {
-			allow = [],
-			deny = [],
-			allow_ip_literals = true,
-		} = serverAclContent;
+		const serverAclContent = aclEvent.getContent() as {
+			allow?: string[];
+			deny?: string[];
+			allow_ip_literals?: boolean;
+		};
+		const allow = Array.isArray(serverAclContent.allow)
+			? serverAclContent.allow
+			: undefined;
+		const deny = Array.isArray(serverAclContent.deny)
+			? serverAclContent.deny
+			: [];
+		const allow_ip_literals = serverAclContent.allow_ip_literals ?? true;
+
+		const hostOnly = this.stripPort(serverName);
 
-		const isIpLiteral =
-			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) ||
-			/^\[.*\](:\d+)?$/.test(serverName); // IPv6
+		const isIpLiteral =
+			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOnly) ||
+			/^\[[^\]]+\]$/.test(hostOnly); // IPv6
 		if (isIpLiteral && !allow_ip_literals) {
 			this.logger.debug(`Server ${serverName} denied: IP literals not allowed`);
 			return false;
 		}
 
 		for (const pattern of deny) {
-			if (this.matchesServerPattern(serverName, pattern)) {
+			if (this.matchesServerPattern(hostOnly, pattern)) {
 				this.logger.debug(
 					`Server ${serverName} matches deny pattern: ${pattern}`,
 				);
 				return false;
 			}
 		}
 
-		// if allow list is empty, deny all servers (as per Matrix spec)
-		// empty allow list means no servers are allowed
-		if (allow.length === 0) {
-			this.logger.debug(`Server ${serverName} denied: allow list is empty`);
-			return false;
-		}
+		// If 'allow' is present and empty -> deny all. If missing -> allow all (subject to deny).
+		if (allow !== undefined && allow.length === 0) {
+			this.logger.debug(`Server ${serverName} denied: allow list is explicitly empty`);
+			return false;
+		}
 
-		for (const pattern of allow) {
-			if (this.matchesServerPattern(serverName, pattern)) {
-				this.logger.debug(
-					`Server ${serverName} matches allow pattern: ${pattern}`,
-				);
-				return true;
-			}
-		}
+		if (allow !== undefined) {
+			for (const pattern of allow) {
+				if (this.matchesServerPattern(hostOnly, pattern)) {
+					this.logger.debug(
+						`Server ${serverName} matches allow pattern: ${pattern}`,
+					);
+					return true;
+				}
+			}
+			this.logger.debug(`Server ${serverName} not in explicit allow list`);
+			return false;
+		}
 
-		this.logger.debug(`Server ${serverName} not in allow list`);
-		return false;
+		// No explicit allow list -> allowed (not denied above)
+		return true;

341-360: Avoid regex-from-input; implement simple '*' wildcard matcher.

Regex construction from ACL patterns risks ReDoS and over-implements the spec.

Apply:

-	private matchesServerPattern(serverName: string, pattern: string): boolean {
-		if (serverName === pattern) {
-			return true;
-		}
-
-		let regexPattern = pattern
-			.replace(/[.+^${}()|[\]\\]/g, '\\$&')
-			.replace(/\*/g, '.*')
-			.replace(/\?/g, '.');
-
-		regexPattern = `^${regexPattern}$`;
-
-		try {
-			const regex = new RegExp(regexPattern);
-			return regex.test(serverName);
-		} catch (error) {
-			this.logger.warn(`Invalid ACL pattern: ${pattern}`, error);
-			return false;
-		}
-	}
+	private matchesServerPattern(serverName: string, pattern: string): boolean {
+		// Exact or match-all
+		if (pattern === '*' || serverName === pattern) return true;
+		// Greedy '*' matcher; other chars are literal
+		let s = 0, p = 0, star = -1, match = 0;
+		while (s < serverName.length) {
+			if (p < pattern.length && pattern[p] === serverName[s]) {
+				s++; p++;
+			} else if (p < pattern.length && pattern[p] === '*') {
+				star = p++;
+				match = s;
+			} else if (star !== -1) {
+				p = star + 1;
+				s = ++match;
+			} else {
+				return false;
+			}
+		}
+		while (p < pattern.length && pattern[p] === '*') p++;
+		return p === pattern.length;
+	}
🧹 Nitpick comments (1)
packages/federation-sdk/src/services/event-authorization.service.ts (1)

195-201: Avoid full-room state fetch on hot path.

Fetching the entire state for ACL and history visibility is heavy. Prefer targeted reads (e.g., repository call by StateMapKey for m.room.server_acl and m.room.history_visibility) or a new getRoomStateByKeys(roomId, keys[]).

Would you like me to draft a minimal StateService.getRoomStateByKeys plus repository query?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 84d007d and d14acf0.

📒 Files selected for processing (3)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (1 hunks)
  • packages/room/src/manager/event-wrapper.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/federation-sdk/src/services/state.service.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.421Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.421Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
packages/federation-sdk/src/services/config.service.ts (2)
  • ConfigService (76-153)
  • serverName (94-96)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 353-353: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🔇 Additional comments (1)
packages/room/src/manager/event-wrapper.ts (1)

178-187: Good addition: precise type guards for ACL and history visibility events.

These integrate cleanly with existing guards and enable safe narrowing downstream. No issues found.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (5)
packages/federation-sdk/src/services/state.service.ts (1)

912-922: Fix server parsing for ports/IPv6 and dedupe results

member.split(':').pop() breaks for IPv6 ([...]:port) and host:port; it also returns duplicates. Parse after the first ":" and return unique servers.

Apply:

-		return members.map((member) => {
-			const server = member.split(':').pop();
-			if (!server) {
-				throw new Error(`Invalid member format of room ${roomId}: ${member}`);
-			}
-			return server;
-		});
+		const servers = new Set<string>();
+		for (const member of members) {
+			const idx = member.indexOf(':');
+			if (idx < 0 || idx === member.length - 1) {
+				throw new Error(`Invalid member format in room ${roomId}: ${member}`);
+			}
+			// Everything after the first ":" is the server name; it may include ":" (port/IPv6).
+			const server = member.slice(idx + 1);
+			servers.add(server);
+		}
+		return [...servers];
packages/federation-sdk/src/services/event-authorization.service.ts (4)

288-339: ACL semantics bug (missing allow should not deny-all) and host handling

Spec: missing allow => allow all (subject to deny and allow_ip_literals). Also perform checks on host-only (no port). Current code denies-all and matches against host:port.

Apply:

-		const serverAclContent = aclEvent.getContent();
-		const {
-			allow = [],
-			deny = [],
-			allow_ip_literals = true,
-		} = serverAclContent;
-
-		const isIpLiteral =
-			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/.test(serverName) ||
-			/^\[.*\](:\d+)?$/.test(serverName); // IPv6
+		const serverAclContent = aclEvent.getContent() as {
+			allow?: string[];
+			deny?: string[];
+			allow_ip_literals?: boolean;
+		};
+		// Distinguish "missing" vs "present but empty"
+		const allow = Array.isArray(serverAclContent.allow)
+			? serverAclContent.allow
+			: undefined;
+		const deny = Array.isArray(serverAclContent.deny)
+			? serverAclContent.deny
+			: [];
+		const allow_ip_literals = serverAclContent.allow_ip_literals ?? true;
+
+		const hostOnly = this.stripPort(serverName);
+		const isIpLiteral =
+			/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostOnly) ||
+			/^\[[^\]]+\]$/.test(hostOnly); // IPv6 [addr]
 		if (isIpLiteral && !allow_ip_literals) {
 			this.logger.debug(`Server ${serverName} denied: IP literals not allowed`);
 			return false;
 		}
 
 		for (const pattern of deny) {
-			if (this.matchesServerPattern(serverName, pattern)) {
+			if (this.matchesServerPattern(hostOnly, pattern)) {
 				this.logger.debug(
 					`Server ${serverName} matches deny pattern: ${pattern}`,
 				);
 				return false;
 			}
 		}
 
-		// if allow list is empty, deny all servers (as per Matrix spec)
-		// empty allow list means no servers are allowed
-		if (allow.length === 0) {
-			this.logger.debug(`Server ${serverName} denied: allow list is empty`);
-			return false;
-		}
+		// If 'allow' is present and empty, deny all. If 'allow' is missing, allow all (subject to deny above).
+		if (allow !== undefined && allow.length === 0) {
+			this.logger.debug(`Server ${serverName} denied: allow list explicitly empty`);
+			return false;
+		}
 
-		for (const pattern of allow) {
-			if (this.matchesServerPattern(serverName, pattern)) {
-				this.logger.debug(
-					`Server ${serverName} matches allow pattern: ${pattern}`,
-				);
-				return true;
-			}
-		}
-
-		this.logger.debug(`Server ${serverName} not in allow list`);
-		return false;
+		if (allow !== undefined) {
+			for (const pattern of allow) {
+				if (this.matchesServerPattern(hostOnly, pattern)) {
+					this.logger.debug(
+						`Server ${serverName} matches allow pattern: ${pattern}`,
+					);
+					return true;
+				}
+			}
+			this.logger.debug(`Server ${serverName} not in explicit allow list`);
+			return false;
+		}
+		// No explicit allow -> allowed
+		return true;

341-360: Avoid regex from untrusted ACL patterns; implement simple '*' matcher

Building RegExp from input risks ReDoS and over-implements spec (only '*' wildcard needed).

Apply:

-	private matchesServerPattern(serverName: string, pattern: string): boolean {
-		if (serverName === pattern) {
-			return true;
-		}
-
-		let regexPattern = pattern
-			.replace(/[.+^${}()|[\]\\]/g, '\\$&')
-			.replace(/\*/g, '.*')
-			.replace(/\?/g, '.');
-
-		regexPattern = `^${regexPattern}$`;
-
-		try {
-			const regex = new RegExp(regexPattern);
-			return regex.test(serverName);
-		} catch (error) {
-			this.logger.warn(`Invalid ACL pattern: ${pattern}`, error);
-			return false;
-		}
-	}
+	private matchesServerPattern(serverName: string, pattern: string): boolean {
+		// Exact or match-all
+		if (serverName === pattern || pattern === '*') return true;
+		// Linear '*' matcher (no regex)
+		const parts = pattern.split('*');
+		let pos = 0;
+		// Prefix check
+		if (!pattern.startsWith('*')) {
+			if (!serverName.startsWith(parts[0])) return false;
+			pos = parts[0].length;
+			parts.shift();
+		}
+		// Middle segments in order
+		for (let i = 0; i < parts.length - 1; i++) {
+			const seg = parts[i];
+			if (!seg) continue;
+			const idx = serverName.indexOf(seg, pos);
+			if (idx === -1) return false;
+			pos = idx + seg.length;
+		}
+		// Suffix check
+		const last = parts[parts.length - 1] ?? '';
+		return pattern.endsWith('*') ? true : serverName.endsWith(last);
+	}

277-280: Do not log full Authorization header

Avoid leaking credentials; redact the header value.

Apply:

-			this.logger.error(
-				{ error, eventId, authorizationHeader, method, uri, body },
+			this.logger.error(
+				{ error, eventId, method, uri, body, authorizationHeader: '<redacted>' },
 				'Error checking event access',
 			);

195-213: Normalize to host-only for checks; compare against normalized membership list

Comparing raw serverName (may include :port) against serversInRoom causes false negatives. Normalize once and use consistently.

Apply:

-			const roomId = event.event.room_id;
+			const roomId = event.event.room_id;
+			const originHost = this.stripPort(serverName);
@@
-			const aclEvent = state.get('m.room.server_acl:');
-			const isServerAllowed = await this.checkServerAcl(aclEvent, serverName);
+			const aclEvent = state.get('m.room.server_acl:');
+			const isServerAllowed = await this.checkServerAcl(aclEvent, originHost);
@@
-			const serversInRoom = await this.stateService.getServersInRoom(roomId);
-			if (serversInRoom.includes(serverName)) {
+			const serversInRoom = await this.stateService.getServersInRoom(roomId);
+			// Normalize membership servers to host-only before comparing
+			if (serversInRoom.some((s) => this.stripPort(s) === originHost)) {
 				this.logger.debug(`Server ${serverName} is in room, allowing access`);
 				return true;
 			}

Add inside the class:

private stripPort(serverName: string): string {
	// [IPv6]:port -> [IPv6]
	if (serverName.startsWith('[')) {
		const m = serverName.match(/^\[([^\]]+)\](?::\d+)?$/);
		return m ? `[${m[1]}]` : serverName;
	}
	// host:port -> host
	return serverName.replace(/:\d+$/, '');
}
🧹 Nitpick comments (2)
packages/room/src/manager/event-wrapper.ts (1)

178-187: LGTM: precise type guards for ACL and history_visibility

Runtime checks align with existing isState() pattern matching.

Consider brief JSDoc on each guard to aid discoverability in IDEs.

Also applies to: 182-187

packages/federation-sdk/src/services/event-authorization.service.ts (1)

196-207: Avoid fetching full room state for ACL and history checks

Fetching the entire state is expensive. Consider a targeted query for just m.room.server_acl and m.room.history_visibility.

Do you want a follow-up patch that adds StateService helpers to fetch specific state keys?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d14acf0 and 1c43cce.

📒 Files selected for processing (3)
  • packages/federation-sdk/src/services/event-authorization.service.ts (2 hunks)
  • packages/federation-sdk/src/services/state.service.ts (1 hunks)
  • packages/room/src/manager/event-wrapper.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-14T13:34:12.448Z
Learnt from: ricardogarim
PR: RocketChat/homeserver#184
File: packages/federation-sdk/src/services/event-authorization.service.ts:213-223
Timestamp: 2025-09-14T13:34:12.448Z
Learning: ACL events (m.room.server_acl) are not yet implemented in the homeserver codebase, so ACL-related code currently handles the simpler case where no ACL events exist in room state.

Applied to files:

  • packages/federation-sdk/src/services/event-authorization.service.ts
🧬 Code graph analysis (1)
packages/federation-sdk/src/services/event-authorization.service.ts (1)
packages/federation-sdk/src/services/config.service.ts (2)
  • ConfigService (76-153)
  • serverName (94-96)
🪛 ast-grep (0.38.6)
packages/federation-sdk/src/services/event-authorization.service.ts

[warning] 353-353: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regexPattern)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

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.

5 participants