Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/meteor/server/settings/federation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,12 @@ export const createFederationServiceSettings = async (): Promise<void> => {
i18nDescription: 'Federation_Service_Matrix_Signing_Key_Description',
public: false,
});

await this.add('Federation_Service_Allow_List', '', {
type: 'string',
i18nLabel: 'Federation_Service_Allow_List',
i18nDescription: 'Federation_Service_Allow_List_Description',
public: false,
});
});
};
2 changes: 2 additions & 0 deletions ee/packages/federation-matrix/src/FederationMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getMatrixRoomsRoutes } from './api/_matrix/rooms';
import { getMatrixSendJoinRoutes } from './api/_matrix/send-join';
import { getMatrixTransactionsRoutes } from './api/_matrix/transactions';
import { getFederationVersionsRoutes } from './api/_matrix/versions';
import { isFederationDomainAllowedMiddleware } from './api/middlewares/isFederationDomainAllowed';
import { isFederationEnabledMiddleware } from './api/middlewares/isFederationEnabled';
import { isLicenseEnabledMiddleware } from './api/middlewares/isLicenseEnabled';
import { registerEvents } from './events';
Expand Down Expand Up @@ -164,6 +165,7 @@ export class FederationMatrix extends ServiceClass implements IFederationMatrixS
matrix
.use(isFederationEnabledMiddleware)
.use(isLicenseEnabledMiddleware)
.use(isFederationDomainAllowedMiddleware)
.use(getMatrixInviteRoutes(this.homeserverServices))
.use(getMatrixProfilesRoutes(this.homeserverServices))
.use(getMatrixRoomsRoutes(this.homeserverServices))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Settings } from '@rocket.chat/core-services';
import { createMiddleware } from 'hono/factory';
import mem from 'mem';

// cache for 60 seconds
const getAllowList = mem(
async () => {
const allowListSetting = await Settings.get<string>('Federation_Service_Allow_List');
return allowListSetting
? allowListSetting
.split(',')
.map((d) => d.trim().toLowerCase())
.filter(Boolean)
: null;
},
{ maxAge: 60000 },
);

/**
* Parses all key-value pairs from a Matrix authorization header.
* Example: X-Matrix origin="matrix.org", key="value", ...
* Returns an object with all parsed values.
*/
// TODO make this function more of a utility if needed elsewhere
function parseMatrixAuthorizationHeader(header: string): Record<string, string> {
const result: Record<string, string> = {};
// Match key="value" pairs
const regex = /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/g;
let match;
while ((match = regex.exec(header)) !== null) {
result[match[1]] = match[2];
}
return result;
}
Comment on lines +25 to +34
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

Header parsing stores case-sensitive keys; normalize to lowercase to avoid origin misses.

If a peer sends Origin= (capitalized), authValues.origin will be undefined.

Apply this diff:

-  while ((match = regex.exec(header)) !== null) {
-    result[match[1]] = match[2];
-  }
+  while ((match = regex.exec(header)) !== null) {
+    result[match[1].toLowerCase()] = match[2];
+  }
📝 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
function parseMatrixAuthorizationHeader(header: string): Record<string, string> {
const result: Record<string, string> = {};
// Match key="value" pairs
const regex = /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/g;
let match;
while ((match = regex.exec(header)) !== null) {
result[match[1]] = match[2];
}
return result;
}
function parseMatrixAuthorizationHeader(header: string): Record<string, string> {
const result: Record<string, string> = {};
// Match key="value" pairs
const regex = /([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"/g;
let match;
while ((match = regex.exec(header)) !== null) {
result[match[1].toLowerCase()] = match[2];
}
return result;
}
🤖 Prompt for AI Agents
In
ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts
around lines 25 to 34, the parseMatrixAuthorizationHeader function stores header
keys with original casing so headers like "Origin" become inaccessible as
"origin"; update the parser to normalize header keys to lowercase when storing
(e.g. use match[1].toLowerCase()) and ensure trimming any surrounding whitespace
before lowercasing so subsequent lookups like authValues.origin succeed
reliably.


export const isFederationDomainAllowedMiddleware = createMiddleware(async (c, next) => {
const allowList = await getAllowList();
if (!allowList || allowList.length === 0) {
// No restriction, allow all
return next();
}

// Extract all key-value pairs from Matrix authorization header
const authHeader = c.req.header('authorization');
if (!authHeader) {
return c.json({ errcode: 'M_UNAUTHORIZED', error: 'Missing Authorization headers.' }, 401);
}

const authValues = parseMatrixAuthorizationHeader(authHeader);
const domain = authValues.origin?.toLowerCase();
if (!domain) {
return c.json({ errcode: 'M_MISSING_ORIGIN', error: 'Missing origin in authorization header.' }, 401);
}
Comment on lines +49 to +53
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

Normalize server name: strip port/brackets; handle IPv6; lowercase.

origin may include a port (e.g., example.org:8448) or IPv6 ([2001:db8::1]:8448); current code will fail matching.

Apply this diff:

+function normalizeServerName(input: string): string | undefined {
+  if (!input) return undefined;
+  let host = input.trim().toLowerCase();
+  // IPv6 in brackets: [::1]:8448
+  if (host.startsWith('[')) {
+    const end = host.indexOf(']');
+    if (end > 0) host = host.slice(1, end);
+  } else {
+    // Strip :port for IPv4/FQDN
+    const idx = host.indexOf(':');
+    if (idx > -1) host = host.slice(0, idx);
+  }
+  // Remove trailing dot
+  return host.replace(/\.$/, '');
+}
@@
-  const authValues = parseMatrixAuthorizationHeader(authHeader);
-  const domain = authValues.origin?.toLowerCase();
+  const authValues = parseMatrixAuthorizationHeader(authHeader);
+  const domain = normalizeServerName(authValues.origin);
   if (!domain) {
     return c.json({ errcode: 'M_MISSING_ORIGIN', error: 'Missing origin in authorization header.' }, 401);
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts
around lines 49-53, the origin from parseMatrixAuthorizationHeader may include a
port or IPv6 brackets and must be normalized before use: strip surrounding
brackets for IPv6, remove an appended port (e.g., "host:8448" -> "host",
"[2001:db8::1]:8448" -> "2001:db8::1"), and lowercase the result; implement this
by checking if origin starts with '[' then extract the substring between '[' and
']', else if origin contains any ':' decide whether it has a port by testing for
a single colon (split on last ':' and take the left-hand side) vs multiple
colons (treat as IPv6 without port and keep whole string), then lowercase and
continue with the existing null/empty check and response.


// Check if domain is in allowed list
if (allowList.some((allowed) => domain.endsWith(allowed))) {
return next();
}
Comment on lines +55 to +58
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

Fix suffix matching to respect label boundaries (security).

endsWith(allowed) lets notdevilevil.com match evil.com. Require exact match or . boundary.

Apply this diff:

-  if (allowList.some((allowed) => domain.endsWith(allowed))) {
+  if (allowList.some((allowed) => domain === allowed || domain.endsWith(`.${allowed}`))) {
     return next();
   }
📝 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
// Check if domain is in allowed list
if (allowList.some((allowed) => domain.endsWith(allowed))) {
return next();
}
// Check if domain is in allowed list
if (allowList.some((allowed) => domain === allowed || domain.endsWith(`.${allowed}`))) {
return next();
}
🤖 Prompt for AI Agents
In
ee/packages/federation-matrix/src/api/middlewares/isFederationDomainAllowed.ts
around lines 55-58, the current check uses domain.endsWith(allowed) which allows
suffix matches across label boundaries (e.g., notdevilevil.com matching
evil.com); change the check to require either an exact match or a dot boundary
by replacing the condition with a normalized comparison like: lowercase/trim
both domain and allowed, then return next() only if domain === allowed ||
domain.endsWith('.' + allowed). This enforces host-label boundaries and avoids
false positive suffix matches.


return c.json({ errcode: 'M_FORBIDDEN', error: 'Federation from this domain is not allowed.' }, 403);
});
4 changes: 3 additions & 1 deletion packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2157,6 +2157,8 @@
"Federation_Service_Alert": "This feature is in beta and may not be stable. Please be aware that it may change, break, or even be removed in the future without any notice.",
"Federation_Service_Matrix_Signing_Key": "Matrix server signing key",
"Federation_Service_Matrix_Signing_Key_Description": "The private signing key used by your Matrix server to authenticate federation requests. Format should be: algorithm version base64<privateKey>. This is typically an Ed25519 algorithm key (version 4), encoded as base64. It is essential for secure communication between federated Matrix servers and should be kept confidential.",
"Federation_Service_Allow_List": "Domain Allow List",
"Federation_Service_Allow_List_Description": "Restrict federation to the given allow list of domains.",
"Field": "Field",
"Field_removed": "Field removed",
"Field_required": "Field required",
Expand Down Expand Up @@ -7023,4 +7025,4 @@
"__usernames__joined": "{{usernames}} joined",
"__usersCount__joined": "{{count}} joined",
"__usersCount__people_will_be_invited": "{{usersCount}} people will be invited"
}
}
Loading