-
Notifications
You must be signed in to change notification settings - Fork 13k
feat: add Federation allow list #37010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Normalize server name: strip port/brackets; handle IPv6; lowercase.
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);
}
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // Check if domain is in allowed list | ||||||||||||||||||
| if (allowList.some((allowed) => domain.endsWith(allowed))) { | ||||||||||||||||||
| return next(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+55
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix suffix matching to respect label boundaries (security).
Apply this diff: - if (allowList.some((allowed) => domain.endsWith(allowed))) {
+ if (allowList.some((allowed) => domain === allowed || domain.endsWith(`.${allowed}`))) {
return next();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| return c.json({ errcode: 'M_FORBIDDEN', error: 'Federation from this domain is not allowed.' }, 403); | ||||||||||||||||||
| }); | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Header parsing stores case-sensitive keys; normalize to lowercase to avoid origin misses.
If a peer sends
Origin=(capitalized),authValues.originwill be undefined.Apply this diff:
📝 Committable suggestion
🤖 Prompt for AI Agents