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
15 changed files with 489 additions and 273 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

@@ -10,15 +10,10 @@ module.exports = ({ config }) => {
// Add the background downloader plugin only for non-TV builds
config.plugins.push("./plugins/withRNBackgroundDownloader.js");
}
// Only override googleServicesFile if env var is set
const androidConfig = {};
if (process.env.GOOGLE_SERVICES_JSON) {
androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON;
}
return {
...(Object.keys(androidConfig).length > 0 && { android: androidConfig }),
android: {
googleServicesFile: process.env.GOOGLE_SERVICES_JSON,
},
...config,
};
};

View File

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

562
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ interface AppleTVCarouselProps {
const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
// Layout Constants
export const APPLE_TV_CAROUSEL_HEIGHT = screenHeight / 1.45;
const CAROUSEL_HEIGHT = screenHeight / 1.45;
const GRADIENT_HEIGHT_TOP = 150;
const GRADIENT_HEIGHT_BOTTOM = 150;
const LOGO_HEIGHT = 80;
@@ -381,7 +381,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
<View
style={{
width: screenWidth,
height: APPLE_TV_CAROUSEL_HEIGHT,
height: CAROUSEL_HEIGHT,
backgroundColor: "#000",
}}
>
@@ -549,7 +549,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
key={item.Id}
style={{
width: screenWidth,
height: APPLE_TV_CAROUSEL_HEIGHT,
height: CAROUSEL_HEIGHT,
position: "relative",
}}
>
@@ -731,7 +731,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
return (
<View
style={{
height: APPLE_TV_CAROUSEL_HEIGHT,
height: CAROUSEL_HEIGHT,
backgroundColor: "#000",
overflow: "hidden",
}}
@@ -749,7 +749,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
return (
<View
style={{
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
backgroundColor: "#000",
overflow: "hidden",
}}
@@ -758,7 +758,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
<Animated.View
style={[
{
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
flexDirection: "row",
width: screenWidth * items.length,
},

View File

@@ -1,19 +1,6 @@
import { LinearGradient } from "expo-linear-gradient";
import type {
MutableRefObject,
PropsWithChildren,
ReactElement,
Ref,
} from "react";
import { useEffect } from "react";
import {
type NativeScrollEvent,
type ScrollViewProps,
type StyleProp,
View,
type ViewProps,
type ViewStyle,
} from "react-native";
import type { PropsWithChildren, ReactElement } from "react";
import { type NativeScrollEvent, View, type ViewProps } from "react-native";
import Animated, {
interpolate,
useAnimatedRef,
@@ -27,9 +14,6 @@ interface Props extends ViewProps {
episodePoster?: ReactElement;
headerHeight?: number;
onEndReached?: (() => void) | null | undefined;
scrollViewProps?: Animated.AnimatedProps<ScrollViewProps>;
contentContainerStyle?: StyleProp<ViewStyle>;
scrollViewRef?: Ref<Animated.ScrollView>;
}
export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
@@ -39,33 +23,10 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
headerHeight = 400,
logo,
onEndReached,
contentContainerStyle,
scrollViewProps,
scrollViewRef,
...props
}: Props) => {
const animatedScrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollViewOffset(animatedScrollRef);
const {
onScroll: externalOnScroll,
style: scrollStyle,
scrollEventThrottle: externalScrollEventThrottle,
...restScrollViewProps
} = scrollViewProps ?? {};
useEffect(() => {
if (!scrollViewRef) return;
const node = animatedScrollRef.current;
if (typeof scrollViewRef === "function") {
scrollViewRef(node);
return () => scrollViewRef(null);
}
(scrollViewRef as MutableRefObject<Animated.ScrollView | null>).current =
node;
}, [animatedScrollRef, scrollViewRef]);
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollViewOffset(scrollRef);
const headerAnimatedStyle = useAnimatedStyle(() => {
return {
@@ -101,17 +62,12 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
return (
<View className='flex-1' {...props}>
<Animated.ScrollView
{...restScrollViewProps}
style={[
{
position: "relative",
},
scrollStyle,
]}
ref={animatedScrollRef}
scrollEventThrottle={externalScrollEventThrottle ?? 16}
style={{
position: "relative",
}}
ref={scrollRef}
scrollEventThrottle={16}
onScroll={(e) => {
externalOnScroll?.(e);
if (isCloseToBottom(e.nativeEvent)) onEndReached?.();
}}
>
@@ -140,12 +96,9 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
</Animated.View>
<View
style={[
{
top: -50,
},
contentContainerStyle,
]}
style={{
top: -50,
}}
className='relative flex-1 bg-transparent pb-24'
>
<LinearGradient

View File

@@ -16,15 +16,14 @@ import { type QueryFunction, useQuery } from "@tanstack/react-query";
import { useNavigation, useRouter, useSegments } from "expo-router";
import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Animated from "react-native-reanimated";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Platform,
RefreshControl,
ScrollView,
TouchableOpacity,
View,
ScrollView,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button";
@@ -39,8 +38,7 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus";
import { ParallaxScrollView } from "@/components/ParallaxPage";
import { AppleTVCarousel, APPLE_TV_CAROUSEL_HEIGHT } from "../AppleTVCarousel";
import { AppleTVCarousel } from "../AppleTVCarousel";
type ScrollingCollectionListSection = {
type: "ScrollingCollectionList";
@@ -68,13 +66,12 @@ export const HomeIndex = () => {
const [loading, setLoading] = useState(false);
const { settings, refreshStreamyfinPluginSettings } = useSettings();
const showCarousel = settings?.showHomeCarousel ?? true;
const navigation = useNavigation();
const insets = useSafeAreaInsets();
const scrollViewRef = useRef<Animated.ScrollView | null>(null);
const scrollViewRef = useRef<ScrollView>(null);
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
const prevIsConnected = useRef<boolean | null>(false);
@@ -131,7 +128,7 @@ export const HomeIndex = () => {
const unsubscribe = eventBus.on("scrollToTop", () => {
if ((segments as string[])[2] === "(home)")
scrollViewRef.current?.scrollTo({
y: 0,
y: Platform.isTV ? -152 : -100,
animated: true,
});
});
@@ -219,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",
@@ -458,36 +453,25 @@ export const HomeIndex = () => {
</View>
);
const headerHeight = showCarousel ? APPLE_TV_CAROUSEL_HEIGHT : 120;
const refreshProgressOffset = showCarousel ? 200 : 80;
return (
<ParallaxScrollView
scrollViewRef={scrollViewRef}
headerHeight={headerHeight}
headerImage={
showCarousel ? (
<AppleTVCarousel initialIndex={0} />
) : (
<View className='flex-1 bg-black' />
)
<ScrollView
scrollToOverflowEnabled={true}
ref={scrollViewRef}
nestedScrollEnabled
contentInsetAdjustmentBehavior='never'
refreshControl={
<RefreshControl
refreshing={loading}
onRefresh={refetch}
tintColor='white' // For iOS
colors={["white"]} // For Android
progressViewOffset={200} // This offsets the refresh indicator to appear over the carousel
/>
}
contentContainerStyle={showCarousel ? undefined : { top: 0 }}
scrollViewProps={{
refreshControl: (
<RefreshControl
refreshing={loading}
onRefresh={refetch}
tintColor='white'
colors={["white"]}
progressViewOffset={refreshProgressOffset}
/>
),
contentInsetAdjustmentBehavior: "never",
nestedScrollEnabled: true,
scrollToOverflowEnabled: true,
}}
style={{ marginTop: Platform.isTV ? 0 : -100 }}
contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }}
>
<AppleTVCarousel initialIndex={0} />
<View
style={{
paddingLeft: insets.left,
@@ -523,7 +507,7 @@ export const HomeIndex = () => {
</View>
</View>
<View className='h-24' />
</ParallaxScrollView>
</ScrollView>
);
};

View File

@@ -62,7 +62,6 @@ export const OtherSettings: React.FC = () => {
pluginSettings?.followDeviceOrientation?.locked === true &&
pluginSettings?.defaultVideoOrientation?.locked === true &&
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
pluginSettings?.showHomeCarousel?.locked === true &&
pluginSettings?.showCustomMenuLinks?.locked === true &&
pluginSettings?.hiddenLibraries?.locked === true &&
pluginSettings?.disableHapticFeedback?.locked === true,
@@ -159,19 +158,6 @@ export const OtherSettings: React.FC = () => {
/>
</ListItem>
<ListItem
title={t("home.settings.other.show_home_carousel")}
disabled={pluginSettings?.showHomeCarousel?.locked}
>
<Switch
value={settings.showHomeCarousel}
disabled={pluginSettings?.showHomeCarousel?.locked}
onValueChange={(value) =>
updateSettings({ showHomeCarousel: value })
}
/>
</ListItem>
{/* {(Platform.OS === "ios" || Platform.isTVOS)&& (
<ListItem
title={t("home.settings.other.video_player")}

View File

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

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.1" },
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.1"`,
}, 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);

View File

@@ -176,7 +176,6 @@
"UNKNOWN": "Unknown"
},
"safe_area_in_controls": "Safe Area in Controls",
"show_home_carousel": "Show Home Carousel",
"video_player": "Video Player",
"video_players": {
"VLC_3": "VLC 3",

View File

@@ -160,7 +160,6 @@ export type Settings = {
subtitleMode: SubtitlePlaybackMode;
rememberSubtitleSelections: boolean;
showHomeTitles: boolean;
showHomeCarousel: boolean;
defaultVideoOrientation: ScreenOrientation.OrientationLock;
forwardSkipTime: number;
rewindSkipTime: number;
@@ -229,7 +228,6 @@ export const defaultValues: Settings = {
subtitleMode: SubtitlePlaybackMode.Default,
rememberSubtitleSelections: true,
showHomeTitles: true,
showHomeCarousel: true,
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
forwardSkipTime: 30,
rewindSkipTime: 10,