diff --git a/src/Connections/FeedConnection.ts b/src/Connections/FeedConnection.ts index 26f427f3..6482831a 100644 --- a/src/Connections/FeedConnection.ts +++ b/src/Connections/FeedConnection.ts @@ -10,6 +10,7 @@ import { GetConnectionsResponseItem } from "../provisioning/api"; import { readFeed, sanitizeHtml } from "../libRs"; import UserAgent from "../UserAgent"; import { retry, retryMatrixErrorFilter } from "../PromiseUtil"; +import { BridgeConfigFeeds } from "../config/Config"; const log = new Logger("FeedConnection"); const md = new markdown({ html: true, @@ -64,7 +65,7 @@ export class FeedConnection extends BaseConnection implements IConnection { return new FeedConnection(roomId, event.stateKey, event.content, intent); } - static async validateUrl(url: string): Promise { + static async validateUrl(url: string, config: BridgeConfigFeeds): Promise { try { new URL(url); } catch (ex) { @@ -75,6 +76,7 @@ export class FeedConnection extends BaseConnection implements IConnection { await readFeed(url, { userAgent: UserAgent, pollTimeoutSeconds: VALIDATION_FETCH_TIMEOUT_S, + maximumFeedSizeMb: config.maximumFeedSizeMB, }); } catch (ex) { throw new ApiError(`Could not read feed from URL: ${ex.message}`, ErrCode.BadValue); @@ -113,7 +115,7 @@ export class FeedConnection extends BaseConnection implements IConnection { } const state = this.validateState(data); - await FeedConnection.validateUrl(state.url); + await FeedConnection.validateUrl(state.url, config.feeds); const connection = new FeedConnection(roomId, state.url, state, intent); await intent.underlyingClient.sendStateEvent(roomId, FeedConnection.CanonicalEventType, state.url, state); diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts index 9f4893f3..337840e8 100644 --- a/src/Connections/SetupConnection.ts +++ b/src/Connections/SetupConnection.ts @@ -312,7 +312,7 @@ export class SetupConnection extends CommandConnection { // provisionConnection will check it again, but won't give us a nice CommandError on failure try { - await FeedConnection.validateUrl(url); + await FeedConnection.validateUrl(url, this.config.feeds); } catch (err: unknown) { log.debug(`Feed URL '${url}' failed validation: ${err}`); if (err instanceof ApiError) { diff --git a/src/config/Config.ts b/src/config/Config.ts index 374c295a..32089db1 100644 --- a/src/config/Config.ts +++ b/src/config/Config.ts @@ -251,6 +251,7 @@ export interface BridgeConfigFeedsYAML { pollIntervalSeconds?: number; pollConcurrency?: number; pollTimeoutSeconds?: number; + maximumFeedSizeMB?: number; } export class BridgeConfigFeeds { @@ -259,6 +260,9 @@ export class BridgeConfigFeeds { public pollTimeoutSeconds: number; public pollConcurrency: number; + @configKey("The maximum response size of a feed on first load. Oversized responses will prevent a connection from being created.", true) + public maximumFeedSizeMB: number; + constructor(yaml: BridgeConfigFeedsYAML) { this.enabled = yaml.enabled; this.pollConcurrency = yaml.pollConcurrency ?? 4; @@ -266,6 +270,11 @@ export class BridgeConfigFeeds { assert.strictEqual(typeof this.pollIntervalSeconds, "number"); this.pollTimeoutSeconds = yaml.pollTimeoutSeconds ?? 30; assert.strictEqual(typeof this.pollTimeoutSeconds, "number"); + this.maximumFeedSizeMB = yaml.maximumFeedSizeMB ?? 25; + assert.strictEqual(typeof this.maximumFeedSizeMB, "number"); + if (this.maximumFeedSizeMB < 1) { + throw new ConfigError('feeds.maximumFeedSizeMB', 'Must be at least 1MB or greater'); + } } @hideKey() diff --git a/src/config/Defaults.ts b/src/config/Defaults.ts index 70522e8e..996fb4e1 100644 --- a/src/config/Defaults.ts +++ b/src/config/Defaults.ts @@ -124,6 +124,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = { pollIntervalSeconds: 600, pollTimeoutSeconds: 30, pollConcurrency: 4, + maximumFeedSizeMB: 25, }, provisioning: { secret: "!secretToken" diff --git a/src/feeds/FeedReader.ts b/src/feeds/FeedReader.ts index 699598ad..f554eec2 100644 --- a/src/feeds/FeedReader.ts +++ b/src/feeds/FeedReader.ts @@ -215,6 +215,7 @@ export class FeedReader { etag, lastModified, userAgent: UserAgent, + maximumFeedSizeMb: this.config.maximumFeedSizeMB, }); // Store any entity tags/cache times. diff --git a/src/feeds/parser.rs b/src/feeds/parser.rs index 631caee8..f0efb7a3 100644 --- a/src/feeds/parser.rs +++ b/src/feeds/parser.rs @@ -37,6 +37,7 @@ pub struct ReadFeedOptions { pub etag: Option, pub poll_timeout_seconds: i64, pub user_agent: String, + pub maximum_feed_size_mb: i64, } #[derive(Serialize, Debug, Deserialize)] @@ -202,6 +203,13 @@ pub async fn js_read_feed(url: String, options: ReadFeedOptions) -> Result { let res_headers = res.headers().clone(); + if res.content_length().unwrap_or(0) > options.maximum_feed_size_mb as u64 { + return Err(JsError::new( + Status::Unknown, + "Feed exceeded maximum size", + )); + } + match res.status() { StatusCode::OK => match res.text().await { Ok(body) => match js_parse_feed(body) {