Compare commits

..

2 Commits

Author SHA1 Message Date
Uruk
2c0ed076d5 fix(security): prevent log injection in WebSocket message logging
Sanitize WebSocket messages before logging to prevent log injection attacks.
User-controlled data from WebSocket messages could contain newline characters
that allow forging fake log entries.

Changes:
- Convert message object to JSON string and remove newlines/carriage returns
- Use format specifier (%s) for safe string interpolation
- Applied fix to providers/WebSocketProvider.tsx and hooks/useWebsockets.ts

Resolves CodeQL security alert js/log-injection

Co-authored-by: GitHub Copilot Autofix <noreply@github.com>
2025-11-07 22:35:53 +01:00
Gauvain
118c24ee05 Potential fix for code scanning alert no. 219: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-10-26 15:32:43 +01:00
11 changed files with 457 additions and 184 deletions

View File

@@ -31,13 +31,13 @@ jobs:
fetch-depth: 0
- name: 🏁 Initialize CodeQL
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: ${{ matrix.language }}
queries: +security-extended,security-and-quality
- name: 🛠️ Autobuild
uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
- name: 🧪 Perform CodeQL Analysis
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0

View File

@@ -1,4 +1,5 @@
name: 🛎️ Discord Notification
permissions: {}
on:
pull_request:

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "Streamyfin",
"slug": "streamyfin",
"version": "0.40.0",
"version": "0.39.0",
"orientation": "default",
"icon": "./assets/images/icon.png",
"scheme": "streamyfin",
@@ -37,7 +37,7 @@
},
"android": {
"jsEngine": "hermes",
"versionCode": 72,
"versionCode": 71,
"adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -20,8 +20,7 @@ const Page: React.FC = () => {
const { offline } = useLocalSearchParams() as { offline?: string };
const isOffline = offline === "true";
const { data: item, isError } = useItemQuery(itemId, false, undefined, [ItemFields.MediaSources]);
const { data: mediaSourcesitem, isError } = useItemQuery(id, isOffline);
const { data: item, isError } = useItemQuery(id, isOffline);
const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => {
@@ -91,7 +90,7 @@ const Page: React.FC = () => {
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
</Animated.View>
{item && <ItemContent item={item} isOffline={isOffline} mediaSourcesItem={mediaSourcesItem} />}
{item && <ItemContent item={item} isOffline={isOffline} />}
</View>
);
};

562
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -216,9 +216,7 @@ export const HomeIndex = () => {
const latestMediaViews = collections.map((c) => {
const includeItemTypes: BaseItemKind[] =
c.CollectionType === "tvshows" || c.CollectionType === "movies"
? []
: ["Movie"];
c.CollectionType === "tvshows" ? ["Series"] : ["Movie"];
const title = t("home.recently_added_in", { libraryName: c.Name });
const queryKey = [
"home",

View File

@@ -45,14 +45,14 @@
},
"production": {
"environment": "production",
"channel": "0.40.0",
"channel": "0.39.0",
"android": {
"image": "latest"
}
},
"production-apk": {
"environment": "production",
"channel": "0.40.0",
"channel": "0.39.0",
"android": {
"buildType": "apk",
"image": "latest"
@@ -60,7 +60,7 @@
},
"production-apk-tv": {
"environment": "production",
"channel": "0.40.0",
"channel": "0.39.0",
"android": {
"buildType": "apk",
"image": "latest"

View File

@@ -1,56 +1,31 @@
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { ItemFields } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
// Helper to exclude specific fields
export const excludeFields = (fieldsToExclude: ItemFields[]) => {
return Object.values(ItemFields).filter(
(field) => !fieldsToExclude.includes(field)
);
};
export const useItemQuery = (
itemId: string | undefined,
isOffline?: boolean,
fields?: ItemFields[],
excludeFields?: ItemFields[]
) => {
export const useItemQuery = (itemId: string, isOffline: boolean) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { getDownloadedItemById } = useDownload();
// Calculate final fields: use excludeFields if provided, otherwise use fields
const finalFields = excludeFields
? Object.values(ItemFields).filter(field => !excludeFields.includes(field))
: fields;
return useQuery({
queryKey: ["item", itemId, finalFields],
queryKey: ["item", itemId],
queryFn: async () => {
if (!itemId) throw new Error('Item ID is required');
if (isOffline) {
return getDownloadedItemById(itemId)?.item;
}
if (!api || !user) return null;
const response = await getUserLibraryApi(api).getItem({
itemId,
userId: user.Id,
...(finalFields && { fields: finalFields }),
if (!api || !user || !itemId) return null;
const res = await getUserLibraryApi(api).getItem({
itemId: itemId,
userId: user?.Id,
});
return response.data;
return res.data;
},
enabled: !!itemId,
staleTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
networkMode: "always",
});
};
};

View File

@@ -96,7 +96,9 @@ export const useWebSocket = ({
| Record<string, string>
| undefined; // Arguments are Dictionary<string, string>
console.log("[WS] ~ ", lastMessage);
// Sanitize output to avoid log injection
const msgStr = JSON.stringify(lastMessage).replaceAll(/[\n\r]/g, " ");
console.log("[WS] ~ %s", msgStr);
if (command === "PlayPause") {
console.log("Command ~ PlayPause");

View File

@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin(
() =>
new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.40.0" },
clientInfo: { name: "Streamyfin", version: "0.39.0" },
deviceInfo: {
name: deviceName,
id,
@@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return {
authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.40.0"`,
}, DeviceId="${deviceId}", Version="0.39.0"`,
};
}, [deviceId]);

View File

@@ -96,7 +96,9 @@ export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
newWebSocket.onmessage = (e) => {
try {
const message = JSON.parse(e.data);
console.log("[WS] Received message:", message);
// Sanitize output to avoid log injection
const msgStr = JSON.stringify(message).replaceAll(/[\n\r]/g, " ");
console.log("[WS] Received message: %s", msgStr);
setLastMessage(message); // Store the last message in context
} catch (error) {
console.error("Error parsing WebSocket message:", error);