diff --git a/src/lib/components/chat/Settings/Account.svelte b/src/lib/components/chat/Settings/Account.svelte index 5d8d873d0..136d42230 100644 --- a/src/lib/components/chat/Settings/Account.svelte +++ b/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 @@ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index c09b844c8..ee3b4a77e 100644 --- a/src/lib/utils/index.ts +++ b/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); diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index b40882ff2..c7492cb26 100644 --- a/src/routes/auth/+page.svelte +++ b/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); };