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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +9 to +10
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid quoted value to satisfy dotenv-linter and reduce copy/paste confusion.
The example value works without quotes; dropping them removes the lint warning.

🛠️ Suggested tweak
-VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com"
+VITE_TRUSTED_MEDIA_URLS=https://matterbridge.example.com,https://matrix-media.example.com
📝 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
# 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"
# 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
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 10-10: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In @.env.example around lines 9 - 10, Remove the surrounding quotes from the
example value for the VITE_TRUSTED_MEDIA_URLS variable in .env.example (update
VITE_TRUSTED_MEDIA_URLS="https://matterbridge.example.com,https://matrix-media.example.com"
to
VITE_TRUSTED_MEDIA_URLS=https://matterbridge.example.com,https://matrix-media.example.com)
so it satisfies dotenv-linter and avoids copy/paste confusion while keeping the
same comma-separated trusted URLs.

11 changes: 11 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 .
```

Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./
Expand Down
21 changes: 18 additions & 3 deletions src/components/message/LinkPreview.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,6 +13,7 @@ interface LinkPreviewProps {
imageUrl?: string;
theme: string;
messageContent: string;
serverId?: string;
}

export const LinkPreview: React.FC<LinkPreviewProps> = ({
Expand All @@ -19,11 +22,16 @@ export const LinkPreview: React.FC<LinkPreviewProps> = ({
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) {
Expand All @@ -37,6 +45,13 @@ export const LinkPreview: React.FC<LinkPreviewProps> = ({
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);
Expand Down Expand Up @@ -76,7 +91,7 @@ export const LinkPreview: React.FC<LinkPreviewProps> = ({
}}
>
<div className="flex items-start h-full">
{imageUrl && showExternalContent && (
{shouldShowImage && (
<div
className="relative inline-block h-full"
style={{ verticalAlign: "top" }}
Expand Down
10 changes: 4 additions & 6 deletions src/components/message/MessageAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type React from "react";
import { useState } from "react";
import { isUrlFromFilehost } from "../../lib/ircUtils";
import { isUrlFromTrustedSource } from "../../lib/ircUtils";
import { mediaLevelToSettings } from "../../lib/mediaUtils";
import useStore from "../../store";

Expand Down Expand Up @@ -40,13 +40,11 @@ export const MessageAvatar: React.FC<MessageAvatarProps> = ({
? 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);

Expand Down
1 change: 1 addition & 0 deletions src/components/message/MessageItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,7 @@ export const MessageItem = memo((props: MessageItemProps) => {
imageUrl={message.linkPreviewMeta}
theme={theme}
messageContent={message.content}
serverId={serverId}
/>
)}
</div>
Expand Down
30 changes: 30 additions & 0 deletions src/lib/ircUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions src/lib/mediaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()) : [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Filter empty entries to avoid accidentally trusting all URLs.
If the list contains a trailing comma or blank item, an empty string can sneak into the array. Downstream checks that use prefix matching could then treat every URL as trusted.

✅ Suggested fix
-      '__TRUSTED_MEDIA_URLS__': process.env.VITE_TRUSTED_MEDIA_URLS ? process.env.VITE_TRUSTED_MEDIA_URLS.replace(/^['"]|['"]$/g, '').split(',').map(url => url.trim()) : [],
+      '__TRUSTED_MEDIA_URLS__': process.env.VITE_TRUSTED_MEDIA_URLS
+        ? process.env.VITE_TRUSTED_MEDIA_URLS
+            .replace(/^['"]|['"]$/g, '')
+            .split(',')
+            .map((url) => url.trim())
+            .filter(Boolean)
+        : [],
📝 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
'__TRUSTED_MEDIA_URLS__': process.env.VITE_TRUSTED_MEDIA_URLS ? process.env.VITE_TRUSTED_MEDIA_URLS.replace(/^['"]|['"]$/g, '').split(',').map(url => url.trim()) : [],
'__TRUSTED_MEDIA_URLS__': process.env.VITE_TRUSTED_MEDIA_URLS
? process.env.VITE_TRUSTED_MEDIA_URLS
.replace(/^['"]|['"]$/g, '')
.split(',')
.map((url) => url.trim())
.filter(Boolean)
: [],
🤖 Prompt for AI Agents
In `@vite.config.ts` at line 29, The '__TRUSTED_MEDIA_URLS__' env parsing
currently splits and trims but can leave empty strings (e.g., trailing commas),
so update the expression that constructs this array to remove any empty entries
after trimming (for example by adding a .filter(...) that keeps only non-empty
strings) so downstream prefix checks don't treat an empty string as matching
everything; locate the expression assigning '__TRUSTED_MEDIA_URLS__' in
vite.config.ts and add the filter step after .map(url => url.trim()).

},
// prevent vite from obscuring rust errors
clearScreen: false,
Expand Down
Loading