Jelajahi Sumber

Introduce canvasPixelTest() intended to validate canvas functionality

Browsers and plugins that spoof canvas data produce corrupt images. In attempt to mitigate:

* Add canvasPixelTest() to test a single pixel and test the RGB values
* Test canvasPixelTest() inside generateInitialsImage() and use default `/user.png` if failure detected
* Call canvasPixelTest() directly within settings to avoid setting an invalid image
* Use toast.error() with 10 second autoClose
Self Denial 1 tahun lalu
induk
melakukan
ac9308dbed

+ 8 - 2
src/lib/components/chat/Settings/Account.svelte

@@ -6,7 +6,7 @@
 	import { updateUserProfile } from '$lib/apis/auths';
 
 	import UpdatePassword from './Account/UpdatePassword.svelte';
-	import { generateInitialsImage } from '$lib/utils';
+	import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
 	import { copyToClipboard } from '$lib/utils';
 
 	const i18n = getContext('i18n');
@@ -148,7 +148,13 @@
 				<button
 					class=" text-xs text-gray-600"
 					on:click={async () => {
-						profileImageUrl = generateInitialsImage(name);
+						if (canvasPixelTest()) {
+							profileImageUrl = generateInitialsImage(name);
+						} else {
+							toast.error("Canvas pixel test failed, fingerprint evasion likely. Disable fingerprint evasion and try again!", {
+								autoClose: 1000 * 10,
+							});
+						}
 					}}>{$i18n.t('Use Gravatar')}</button
 				>
 			</div>

+ 40 - 0
src/lib/utils/index.ts

@@ -96,12 +96,52 @@ export const getGravatarURL = (email) => {
 	return `https://www.gravatar.com/avatar/${hash}`;
 };
 
+export const canvasPixelTest = () => {
+	// Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
+	// Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
+	const canvas = document.createElement("canvas");
+	const ctx = canvas.getContext('2d');
+	canvas.height = 1;
+	canvas.width = 1;
+	const imageData = new ImageData(canvas.width, canvas.height);
+	const pixelValues = imageData.data;
+
+	// Generate RGB test data
+	for (let i = 0; i < imageData.data.length; i += 1){
+		if (i % 4 !== 3){
+			pixelValues[i] = Math.floor(256 * Math.random());
+		}
+		else {
+			pixelValues[i] = 255;
+		}
+	}
+
+	ctx.putImageData(imageData, 0, 0);
+	const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+	// Read RGB data and fail if unmatched
+	for (let i = 0; i < p.length; i += 1){
+		if (p[i] !== pixelValues[i]){
+			console.log("canvasPixelTest: Wrong canvas pixel RGB value detected:", p[i], "at:", i, "expected:", pixelValues[i]);
+			console.log("canvasPixelTest: Canvas blocking or spoofing is likely");
+			return false;
+		}
+	}
+
+	return true;
+}
+
 export const generateInitialsImage = (name) => {
 	const canvas = document.createElement('canvas');
 	const ctx = canvas.getContext('2d');
 	canvas.width = 100;
 	canvas.height = 100;
 
+	if (!canvasPixelTest()) {
+		console.log("generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.");
+		return '/user.png';
+	}
+
 	ctx.fillStyle = '#F39C12';
 	ctx.fillRect(0, 0, canvas.width, canvas.height);
 

+ 7 - 1
src/routes/auth/+page.svelte

@@ -5,7 +5,7 @@
 	import { WEBUI_NAME, config, user } from '$lib/stores';
 	import { onMount, getContext } from 'svelte';
 	import { toast } from 'svelte-sonner';
-	import { generateInitialsImage } from '$lib/utils';
+	import { generateInitialsImage, canvasPixelTest } from '$lib/utils';
 
 	const i18n = getContext('i18n');
 
@@ -43,6 +43,12 @@
 			}
 		);
 
+		if (!canvasPixelTest()) {
+			toast.error("Canvas pixel test failed, fingerprint evasion likely. Default image used.", {
+				autoClose: 1000 * 10,
+			});
+		}
+
 		await setSessionUser(sessionUser);
 	};