diff --git a/.env.example b/.env.example index 9ea78bcd..77dc9002 100644 --- a/.env.example +++ b/.env.example @@ -6,3 +6,5 @@ VITE_DEFAULT_IRC_SERVER_NAME="Local" VITE_DEFAULT_IRC_CHANNELS="#lobby,#bots,#test" # Optionally hide the server list VITE_HIDE_SERVER_LIST=true +# Optional comma-separated list of trusted media URLs (for chat bridge image proxies like Matterbridge, Matrix bridges) +VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c9771a7d..9747b995 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -228,6 +228,7 @@ VITE_DEFAULT_IRC_SERVER # Default server URL VITE_DEFAULT_IRC_SERVER_NAME # Server display name VITE_DEFAULT_IRC_CHANNELS # Auto-join channels VITE_HIDE_SERVER_LIST # Hide server selection +VITE_TRUSTED_MEDIA_URLS # Comma-separated list of trusted media URLs ``` ### GitHub Actions CI/CD @@ -275,6 +276,16 @@ npm run tauri android build -- --apk # Android - LocalStorage for user preferences - Build-time configuration injection +### Security & Media Handling +- **Trusted Media Sources**: Images and media are validated against trusted sources + - Server-specific filehost URLs (per-server configuration) + - Global trusted media URLs (build-time configuration via `VITE_TRUSTED_MEDIA_URLS`) + - Useful for chat bridge integrations (Matterbridge, Matrix bridges) +- **Content Display Settings**: + - `showSafeMedia`: Display media from trusted sources only + - `showExternalContent`: Display all external media (requires user confirmation) +- **URL Validation**: `isUrlFromTrustedSource()` validates URLs against all trusted sources + ## 📋 Implementation Guidelines ### Adding New Features diff --git a/BUILD.md b/BUILD.md index ae29c09d..4c624ac4 100644 --- a/BUILD.md +++ b/BUILD.md @@ -27,6 +27,9 @@ VITE_DEFAULT_IRC_SERVER_NAME="Local" VITE_DEFAULT_IRC_CHANNELS="#lobby,#bots,#test" # Optionally hide the server list VITE_HIDE_SERVER_LIST=true +# Optional comma-separated list of trusted media URLs +# Useful for chat bridges like Matterbridge or Matrix bridges that host media +VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com" ``` ### Docker @@ -43,6 +46,7 @@ docker build \ --build-arg VITE_DEFAULT_IRC_SERVER_NAME="Your Server" \ --build-arg VITE_DEFAULT_IRC_CHANNELS="#general,#random" \ --build-arg VITE_HIDE_SERVER_LIST=false \ + --build-arg VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com" \ -t obsidianirc . ``` diff --git a/Dockerfile b/Dockerfile index b14fb82f..22b44aec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,13 @@ ARG VITE_DEFAULT_IRC_SERVER ARG VITE_DEFAULT_IRC_SERVER_NAME ARG VITE_DEFAULT_IRC_CHANNELS ARG VITE_HIDE_SERVER_LIST +ARG VITE_TRUSTED_MEDIA_URLS ENV VITE_DEFAULT_IRC_SERVER=$VITE_DEFAULT_IRC_SERVER ENV VITE_DEFAULT_IRC_SERVER_NAME=$VITE_DEFAULT_IRC_SERVER_NAME ENV VITE_DEFAULT_IRC_CHANNELS=$VITE_DEFAULT_IRC_CHANNELS ENV VITE_HIDE_SERVER_LIST=$VITE_HIDE_SERVER_LIST +ENV VITE_TRUSTED_MEDIA_URLS=$VITE_TRUSTED_MEDIA_URLS WORKDIR /app COPY package*.json ./ diff --git a/src/components/message/LinkPreview.tsx b/src/components/message/LinkPreview.tsx index 69bdab0f..a6324fb5 100644 --- a/src/components/message/LinkPreview.tsx +++ b/src/components/message/LinkPreview.tsx @@ -1,5 +1,7 @@ import type React from "react"; import { useState } from "react"; +import { isUrlFromTrustedSource } from "../../lib/ircUtils"; +import { mediaLevelToSettings } from "../../lib/mediaUtils"; import { stripIrcFormatting } from "../../lib/messageFormatter"; import { openExternalUrl } from "../../lib/openUrl"; import useStore from "../../store"; @@ -11,6 +13,7 @@ interface LinkPreviewProps { imageUrl?: string; theme: string; messageContent: string; + serverId?: string; } export const LinkPreview: React.FC = ({ @@ -19,11 +22,16 @@ export const LinkPreview: React.FC = ({ imageUrl, theme, messageContent, + serverId, }) => { const [showWarningModal, setShowWarningModal] = useState(false); - const showExternalContent = - useStore((state) => state.globalSettings.mediaVisibilityLevel) >= 3; + const { showSafeMedia, showExternalContent } = mediaLevelToSettings( + useStore((state) => state.globalSettings.mediaVisibilityLevel), + ); + const server = serverId + ? useStore.getState().servers.find((s) => s.id === serverId) + : null; // Don't render if there's no content to show if (!title && !snippet && !imageUrl) { @@ -37,6 +45,13 @@ export const LinkPreview: React.FC = ({ const match = cleanContent.match(urlRegex); const firstUrl = match ? match[0] : undefined; + // Check if image is from a trusted source (server filehost or globally configured trusted URLs) + const isTrustedImage = + imageUrl && isUrlFromTrustedSource(imageUrl, server?.filehost); + // Show image if it's from a trusted source and safe media is enabled, or if external content is allowed + const shouldShowImage = + imageUrl && ((isTrustedImage && showSafeMedia) || showExternalContent); + const handleClick = () => { if (firstUrl) { setShowWarningModal(true); @@ -76,7 +91,7 @@ export const LinkPreview: React.FC = ({ }} >
- {imageUrl && showExternalContent && ( + {shouldShowImage && (
= ({ ? useStore.getState().servers.find((s) => s.id === serverId) : null; - const isFilehostAvatar = - avatarUrl && - server?.filehost && - isUrlFromFilehost(avatarUrl, server.filehost); + const isTrustedAvatar = + avatarUrl && isUrlFromTrustedSource(avatarUrl, server?.filehost); const shouldShowAvatar = avatarUrl && - ((isFilehostAvatar && showSafeMedia) || + ((isTrustedAvatar && showSafeMedia) || showTrustedSourcesMedia || showExternalContent); diff --git a/src/components/message/MessageItem.tsx b/src/components/message/MessageItem.tsx index e29eebaa..6b6be081 100644 --- a/src/components/message/MessageItem.tsx +++ b/src/components/message/MessageItem.tsx @@ -840,6 +840,7 @@ export const MessageItem = memo((props: MessageItemProps) => { imageUrl={message.linkPreviewMeta} theme={theme} messageContent={message.content} + serverId={serverId} /> )}
diff --git a/src/lib/ircUtils.tsx b/src/lib/ircUtils.tsx index eae3eca8..63bd2949 100644 --- a/src/lib/ircUtils.tsx +++ b/src/lib/ircUtils.tsx @@ -968,6 +968,36 @@ export function isUrlFromFilehost( } } +/** + * Check if a URL is from any of the trusted media sources + * This includes both the server's filehost and globally configured trusted URLs + */ +export function isUrlFromTrustedSource( + url: string, + serverFilehost?: string, +): boolean { + if (!url) return false; + + // Check against server filehost + if (serverFilehost && isUrlFromFilehost(url, serverFilehost)) { + return true; + } + + // Check against globally configured trusted media URLs + const trustedUrls = __TRUSTED_MEDIA_URLS__; + if (!trustedUrls || trustedUrls.length === 0) { + return false; + } + + for (const trustedUrl of trustedUrls) { + if (trustedUrl && isUrlFromFilehost(url, trustedUrl)) { + return true; + } + } + + return false; +} + /** * Checks if a server is running UnrealIRCd based on the RPL_YOURHOST response * @param server The server object to check diff --git a/src/lib/mediaUtils.ts b/src/lib/mediaUtils.ts index 62052a30..79ec1761 100644 --- a/src/lib/mediaUtils.ts +++ b/src/lib/mediaUtils.ts @@ -281,8 +281,12 @@ export function canShowMedia( filehost?: string | null, ): boolean { if (settings.showExternalContent) return true; - if (settings.showSafeMedia && filehost && isUrlFromFilehost(url, filehost)) - return true; + if (settings.showSafeMedia) { + if (filehost && isUrlFromFilehost(url, filehost)) return true; + for (const trustedUrl of __TRUSTED_MEDIA_URLS__) { + if (trustedUrl && isUrlFromFilehost(url, trustedUrl)) return true; + } + } if (settings.showTrustedSourcesMedia) { const hostname = extractHostname(url); if (hostname === null) return false; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 1831444d..fd2362f5 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -7,3 +7,4 @@ declare const __DEFAULT_IRC_SERVER_NAME__: string; declare const __DEFAULT_IRC_CHANNELS__: string[]; declare const __HIDE_SERVER_LIST__: boolean; declare const __BACKEND_URL__: string; +declare const __TRUSTED_MEDIA_URLS__: string[]; diff --git a/vite.config.ts b/vite.config.ts index 5482909e..4ec1662c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig(({ mode }) => { '__DEFAULT_IRC_CHANNELS__': process.env.VITE_DEFAULT_IRC_CHANNELS ? process.env.VITE_DEFAULT_IRC_CHANNELS.replace(/^['"]|['"]$/g, '').split(',').map(ch => ch.trim()) : [], '__HIDE_SERVER_LIST__': process.env.VITE_HIDE_SERVER_LIST === 'true', '__BACKEND_URL__': JSON.stringify(process.env.VITE_BACKEND_URL || 'http://localhost:8080'), + '__TRUSTED_MEDIA_URLS__': process.env.VITE_TRUSTED_MEDIA_URLS ? process.env.VITE_TRUSTED_MEDIA_URLS.replace(/^['"]|['"]$/g, '').split(',').map(url => url.trim()) : [], }, // prevent vite from obscuring rust errors clearScreen: false,