4_ways_to_align_with_WebGL_fingerprinting_in_web_scraping

WebGLフィンガープリントは、自動化されたアクセスを検知するため、アンチボットシステムで広く使われている識別技術です。ハードウェアレベルのシグナルを利用するため回避が難しく、スクレイピングを行う際の新たな課題となります。それでも、目立たずにデータを収集する方法はあります。詳しく見ていきましょう。

WebGLとは: 詳しく解説

WebGLはWeb Graphics Libraryの略で、JavaScriptのAPIです。ブラウザはこれを使って、プラグインをインストールせずに動的な2Dおよび3Dグラフィックスを表示します。こうしたレンダリング処理を行うために、このAPIはGPU(Graphics Processing Unit)に大きく依存します。今日のウェブサイトは、視覚効果やゲーム、複雑なグラフィカルインターフェースにWebGLを幅広く活用しています。実際、WebGL 2はより多くの機能を提供するため、現在主流になりつつあります。Chrome、Edge、SafariなどのモダンブラウザではWebGL 2の対応率が95~99%に達していますが、古いブラウザやヘッドレスブラウザでは依然として対応していない場合があります。

利用可能なGPUは数百種類あり、同じメーカーのモデルでも大きく異なるため、同じ画像でもレンダリング結果は異なります。さらに、ブラウザ、インストールされているドライバー、そしてGPUとマザーボードの接続方法といったハードウェアの違いも結果に影響します。これは、家電量販店で同じ広告を映している何十台ものテレビを見たときに、それぞれの画が少しずつ違って見えるのと似ています。こうした違いがWebGLフィンガープリントの基盤になります。WebGLフィンガープリントはJavaScriptが実行される場合にのみ取得できます。静的なHTMLページだけでは実行できません。

WebGLフィンガープリント: 手順ごとの概要

おおまかに言うと、全体のプロセスは次のようになります。

  1. ウェブサイトはページのHTML内に非表示の<canvas>要素を作成し、WebGLのレンダリング環境を初期化します。
  2. JavaScriptはgl.getParameter()を呼び出し、ベンダーやモデルなどのGPU特性を取得します。
  3. フィンガープリントスクリプトはブラウザにテスト画像をレンダリングさせ、readPixels()を使ってピクセルデータを抽出します。

BrowserLeaksにアクセスしてWebGL Reportをクリックすると、固有のフィンガープリントを確認できます。表示例は次のとおりです。

以下に、ウェブサイトが使用するスクリプトの例を示します。診断、バグ報告、互換性テストなど、テスト目的で使用することもできます。スクリプトは、自分が所有するデバイス上で実行するか、明示的な同意を得てから実行してください。トラッキングや非匿名化の目的でスクリプトを使用することは避けてください。結果は機密データとして扱い、許可を得ない限り第三者と共有しないでください。DataImpulseはスクリプトの違法な使用について責任を負いません。

ステップ1. WebGL環境を起動する


// Lightweight FNV-1a hash for strings/byte arrays (synchronous, simple)
function fnv1aHashFromBytes(bytes) {
  let h = 0x811c9dc5 >>> 0;
  for (let i = 0; i < bytes.length; i++) {
    h ^= bytes[i];
    // multiply by FNV prime (mod 2^32)
    h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
  }
  return ('00000000' + (h >>> 0).toString(16)).slice(-8);
}
function fnv1aHashFromString(s) {
  const bytes = new Uint8Array(s.length);
  for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i) & 0xff;
  return fnv1aHashFromBytes(bytes);
}
// Create an (optionally hidden) canvas and return WebGLRenderingContext / WebGL2RenderingContext
function createWebGLContext(opts = {webgl2Preferred: true, preserveDrawingBuffer: false}) {
  const canvas = document.createElement('canvas');
  canvas.width = 256; canvas.height = 256;
  // optionally keep canvas off-DOM so it stays invisible
  canvas.style.display = 'none';
  document.body && document.body.appendChild(canvas); // append only if available
  let gl = null;
  if (opts.webgl2Preferred) {
    try { gl = canvas.getContext('webgl2', {preserveDrawingBuffer: opts.preserveDrawingBuffer}); } catch (e) {}
  }
  if (!gl) {
    try { gl = canvas.getContext('webgl', {preserveDrawingBuffer: opts.preserveDrawingBuffer}) || canvas.getContext('experimental-webgl'); } catch (e) {}
  }
  return {gl, canvas};
}

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
      

このコードは、バックグラウンドで動作する非表示のcanvasを作成し、その解像度を設定します。また、WebGL 2が利用できない場合は、WebGLへ適切にフォールバックします。

ステップ2. ハードウェア情報を取得する


function getWebGLHardwareInfo(gl) {
  const info = {};
  // Standard parameters
  info.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
  info.maxCombinedTextureImageUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
  info.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
  info.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
  info.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
  info.shaderPrecision = {
    vertexHighFloat: gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT),
    fragmentHighFloat: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT)
  };
  // Try to get unmasked renderer/vendor (if browser exposes WEBGL_debug_renderer_info)
  const dbg = gl.getExtension('WEBGL_debug_renderer_info');
  if (dbg) {
    info.unmaskedVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
    info.unmaskedRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
  } else {
    info.unmaskedVendor = null;
    info.unmaskedRenderer = null;
  }
  // Extensions list (useful fingerprinting signal)
  info.extensions = gl.getSupportedExtensions() || [];
  // Anisotropy (if available)
  const extAniso = gl.getExtension('EXT_texture_filter_anisotropic') ||
                   gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
                   gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
  if (extAniso) {
    const maxAniso = gl.getParameter(extAniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
    info.maxAnisotropy = maxAniso;
  }
  return info;
}

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        

        
      

ベンダー、モデル、メモリサイズ、最大テクスチャサイズ、シェーダーのユニフォーム制限、サポートされているWebGL拡張機能など、GPUに関するさまざまな詳細情報を収集します。

ステップ3. シェーダーの精度を調べる


function probeShaderPrecisions(gl) {
  const precisions = {};
  const shaderTypes = [
    {type: gl.VERTEX_SHADER, name: 'vertex'},
    {type: gl.FRAGMENT_SHADER, name: 'fragment'}
  ];
  const levels = [{p: gl.LOW_FLOAT, name: 'low'}, {p: gl.MEDIUM_FLOAT, name: 'medium'}, {p: gl.HIGH_FLOAT, name: 'high'}];
  for (const sh of shaderTypes) {
    precisions[sh.name] = {};
    for (const lv of levels) {
      const pf = gl.getShaderPrecisionFormat(sh.type, lv.p);
      precisions[sh.name][lv.name] = {rangeMin: pf.rangeMin, rangeMax: pf.rangeMax, precision: pf.precision};
    }
  }
  return precisions;
}

      
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
      

次にウェブサイトはGPUに対し、浮動小数点演算とフラグメントシェーダーでどの精度レベルがサポートされているかを問い合わせ、その数値範囲を記録します。

スクリプトはハードウェアとソフトウェアの情報を収集した後、次の段階である画像のレンダリングに進みます。

ステップ4. テストシーンのレンダリング


// Simple shader pair: position-only vertex shader and color-fragment that depends on coords
const VERT = `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() {
  v_uv = a_pos * 0.5 + 0.5;
  gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const FRAG = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
void main() {
  // deterministic function of coordinates that will exercise GPU math
  float r = sin(v_uv.x * 123.456) * 0.5 + 0.5;
  float g = cos(v_uv.y * 78.9) * 0.5 + 0.5;
  float b = fract((v_uv.x + v_uv.y) * 9876.54321);
  outColor = vec4(r, g, b, 1.0);
}`;

function createProgramWebGL2(gl, vertSrc, fragSrc) {
  function compile(src, type) {
    const s = gl.createShader(type);
    gl.shaderSource(s, src);
    gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
      const err = gl.getShaderInfoLog(s);
      gl.deleteShader(s);
      throw new Error('Shader compile error: ' + err);
    }
    return s;
  }
  const vs = compile(vertSrc, gl.VERTEX_SHADER);
  const fs = compile(fragSrc, gl.FRAGMENT_SHADER);
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    const err = gl.getProgramInfoLog(program);
    throw new Error('Program link error: ' + err);
  }
  gl.deleteShader(vs);
  gl.deleteShader(fs);
  return program;
}

function renderTestAndHash(gl, canvas) {
  const isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
  if (!isWebGL2) {
    // For WebGL1 we'd need slightly different shaders (no #version, use attribute/varying)
    // but the concept is the same. For clarity this example uses WebGL2 if available.
    console.warn('Example uses WebGL2 for deterministic shader pipeline; falling back may require shader edits.');
  }

  // Create program & buffer
  const prog = createProgramWebGL2(gl, VERT, FRAG);
  gl.useProgram(prog);
  const posLoc = gl.getAttribLocation(prog, 'a_pos');

  const quad = new Float32Array([
    -1, -1,
     1, -1,
    -1,  1,
     1,  1
  ]);
  const vb = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vb);
  gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(posLoc);
  gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);

  // Render to the canvas
  gl.viewport(0, 0, canvas.width, canvas.height);
  gl.clearColor(0,0,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  const t0 = performance.now();
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  gl.finish(); // ensure rendering completes
  const t1 = performance.now();

  const renderTime = t1 - t0; // render timing in ms

  // Read pixels
  const px = new Uint8Array(canvas.width * canvas.height * 4);
  gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, px);

  const pixelHash = fnv1aHashFromBytes(px);

  // Clean up (optional)
  gl.deleteBuffer(vb);
  gl.deleteProgram(prog);

  return {pixelHash, renderTime};
}

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
      

スクリプトはあなたのブラウザに、特定の陰影、グラデーション、効果を含む2Dまたは3Dシーンをレンダリングするよう要求します。次にピクセルのバイト列を読み取り、色、テクスチャ、シェーディング、グラデーションなどの値からハッシュを計算し、レンダリング時間を測定します。

ステップ5. データをフィンガープリントに結合する


function collectWebGLFingerprint(opts = {}) {
  const {gl, canvas} = createWebGLContext({webgl2Preferred: true, preserveDrawingBuffer: true});
  if (!gl) throw new Error('No WebGL available');

  const hw = getWebGLHardwareInfo(gl);
  const precisions = probeShaderPrecisions(gl);

  // optional: probe more capabilities
  const supportsFloatTextures = !!gl.getExtension('OES_texture_float') || !!gl.getExtension('EXT_color_buffer_float');

  // Render test and get pixel hash + timing
  const {pixelHash, renderTime} = renderTestAndHash(gl, canvas);

  // Combine into a fingerprint string
  const parts = [
    navigator.userAgent || '',
    hw.unmaskedVendor || '',
    hw.unmaskedRenderer || '',
    hw.maxTextureSize,
    hw.maxCombinedTextureImageUnits,
    hw.maxVertexAttribs,
    supportsFloatTextures ? 'floatTex' : '',
    hw.extensions ? hw.extensions.join(',') : '',
    JSON.stringify(precisions),
    pixelHash,
    String(Math.round(renderTime)) // include rounded render time
  ];
  const raw = parts.join('|');
  const fingerprint = fnv1aHashFromString(raw);

  // Return a readable object
  return {
    fingerprint,
    raw,
    components: {
      ua: navigator.userAgent,
      hardware: hw,
      precisions,
      pixelHash,
      renderTime
    }
  };
}

// Example usage:
try {
  const fp = collectWebGLFingerprint();
  console.log('WebGL fingerprint:', fp.fingerprint);
  console.log('Components:', fp.components);
} catch (e) {
  console.error('Could not collect WebGL fingerprint:', e);
}

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

最後に、スクリプトはこれまでのすべてのステップを呼び出し、取得したすべてのデータを1つの長い文字列に結合し、さらにハッシュ化して、よりコンパクトで扱いやすいフィンガープリントIDを作成します。

この一連のプロセスはミリ秒単位で行われ、ユーザーには見えません。フィンガープリントを収集した後、ウェブサイトはそれを保存して内部データベースと照合するか、分析のためにアンチボットサービスへ送信します。フィンガープリントが既知のボットや不審なユーザーと一致した場合、アカウント停止やアクセス禁止を受けるリスクがあります。

WebGLスプーフィングの課題

トラフィックの特徴を自然に見せることが難しい理由はいくつかあります。

  1. GPUのレンダリング挙動の違いは人間の目には見えないかもしれませんが、ウェブサイトには明確に判別でき、ページをまたいでも一貫しています。Cookieを消去したりシークレットモードを使ったりしても役に立ちません。
  2. レンダリングは物理的なハードウェアから得られるデータに依存しており、これはJavaScriptによる改変やブラウザ拡張機能では手が届きません。
  3. ページを読み込むたびにWebGLのパラメータを変更することはできます。しかし、自分のGPUがIntelだと主張しているのにNVIDIAのようにレンダリングされると、ウェブサイトはその不整合を検知でき、それだけでアンチボットシステムが作動する理由になり得ます。
  4. 最新の検知システムの大半は、WebGL APIを改ざんしようとする試みを捕捉します。

では、WebGLフィンガープリントに対処する方法はないのでしょうか。いいえ、実際に有効な手法はいくつかあります。順を追って見ていきましょう。

WebGLフィンガープリントのスプーフィングは合法か?

あなたには自分のプライバシーを守る権利があるため、WebGLフィンガープリントのスプーフィングが違法だと言うのは誤りです。テスト目的でも使われています。しかし、詐欺を働いたりウェブサイトの利用規約に違反したりするためにスプーフィングを利用することは法律に反します。常に利用規約を読み、他者のプライバシーを害しかねない違法行為は避けるべきです。 以下の情報は、あくまで教育目的で提供されています。DataImpulseはいかなる行動も推奨しておらず、起こりうる結果について責任を負いません。

WebGLフィンガープリントをスプーフィングする実用的な方法

PlaywrightやSeleniumなどの一般的なツールでは、Playwright StealthやUndetected Chrome Driverなどを使って、画面サイズなどのパラメータをわずかに変更できます。ただし、偽の変数とレンダリングされた画像が互いに一致するよう注意する必要があります。この方法は大規模なプロジェクトや、最新のボット検知アルゴリズムを使うウェブサイトをスクレイピングしようとする場合には機能しません。WebGL Fingerprint DefenderやCanvasBlockerといったブラウザ拡張機能についても同じことが言えます。これらはWebGL APIの呼び出しを傍受して改変したデータを返しますが、レンダリングされた画像は依然として一致しない可能性があります。

  • GPU仮想化を利用する

ブラウザのGPUやドライバーを、実際の物理GPUとは異なるように見せることができます。利用できるアプローチはいくつかあります。たとえば、実際のGPUをSwiftShaderのようなソフトウェアレンダラーに置き換える方法や、さまざまなGPUドライバーを備えた仮想マシンを使う方法があります。一方で、予算が限られていたり高度な技術スキルが不足していたりする場合は、これらの方法を採用する前に慎重に検討してください。さらに、スクレイピングプロジェクトを拡張するのは難しい作業になりえます。

  • APIを利用する

この方法は、公開データをスクレイピングしていて、対象のウェブサイトがJSONを返す公開または非公開のAPIを提供している場合に機能します。保護された動的なウェブサイトでは選択肢になりませんが、それ以外の場合、APIエンドポイントは一般に保護が比較的薄いため、有力な選択肢になり得ます。

  • WebGLを無効化する

WebGLを無効化すると、主要なWebGLフィンガープリントのシグナル(vendor/renderer、拡張機能、精度、ピクセルハッシュ、レンダリング時間)がすべて遮断されます。

WebGLを無効化する方法

Chrome

方法1: Chrome Flagsを使う

アドレスバーにchrome://flagsと入力してEnterキーを押し、“Disable WebGL”または–disable-webglを選択します。フラグを”Disabled”に設定し、変更を有効にするためChromeを再起動します。

方法2: コマンドラインを使う

Google Chromeのアイコンを右クリックし、「プロパティ」を開きます。「リンク先」の行を探します。この行はchrome.exe.のような文字列で終わっているはずです。行末にスペースを追加し、–disable-webglと入力します。「適用」>「OK」をクリックします。

Firefox

アドレスバーにabout:configと入力してEnterキーを押します。警告が表示されたら同意し、webgl.disableを探します。ダブルクリックして値をtrueに設定します。

Safari

Safariの環境設定を開き、「詳細」タブで「開発」を選択します(利用可能な場合)。実験的機能に移動し、WebGL via Metalのオプションを見つけます。チェックボックスのチェックを外し、ブラウザを再起動します。

ただし、WebGLを無効化すると多くのウェブサイトが正しく機能しなくなる可能性があるため、注意してください。

WebGLフィンガープリントの弱点を利用する

ちょっとしたテクニックを使うことができます。例えば、スクリプトは結果をキャッシュし、最初から再計算する代わりにそれを再利用することがよくあります。この挙動を検知し、キャッシュが再利用される前に環境を変更して以前に記録された識別情報との一致を防ぐか、サイトに値を再計算させることができます。

検出器:


// Helper: run the fingerprinting routine and return {time, result}
async function runFingerprintTrial(triggerFn) {
  const t0 = performance.now();
  const result = await triggerFn(); // should return fingerprint/hash if available
  const time = performance.now() - t0;
  return { time, result };
}

async function detectCachingRobust(triggerFingerprinting, trials = 5) {
  const runs = [];
  for (let i = 0; i < trials; i++) {
    // small delay to reduce micro-scheduling bias
    if (i > 0) await new Promise(r => setTimeout(r, 30));
    runs.push(await runFingerprintTrial(triggerFingerprinting));
  }

  // compute median time
  const times = runs.map(r => r.time).sort((a,b) => a-b);
  const medianTime = times[Math.floor(times.length/2)];

  // compare first vs median of later runs
  const firstTime = runs[0].time;
  const laterMedian = times.slice(1).length ? times.slice(1)[Math.floor((times.length-1)/2)] : medianTime;

  // check result stability (if trigger returns a fingerprint/hash)
  const results = runs.map(r => r.result);
  const allSame = results.every(x => x === results[0]);

  // simple heuristic: if first run is much slower *and* results identical, likely cached
  const speedRatio = firstTime / (laterMedian || 1);
  const likelyCached = (speedRatio > 5) && allSame;

  return {
    runs,
    medianTime,
    firstTime,
    laterMedian,
    speedRatio,
    results,
    allSame,
    likelyCached
  };
}

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        

        
        
        
        
        

        
        
        
        
        
      

新しい環境を強制する:


function newWebGLContext(w=256,h=256) {
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  const gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl');
  return {c, gl};
}

      
        
        
        
        
        
        
      

再計算を強制する:


localStorage.removeItem('fp_token'); // only if you can identify it
// or flush likely storage (be careful: may break site)
localStorage.clear();
indexedDB.deleteDatabase('site-db-name'); // needs precise names

      
        
        
        
        
      

もう一つのアプローチは、ページがフィンガープリントスクリプトを追加するタイミングを検知し、それに先立ってスプーフィングスクリプトを実行することです。ただし、これは容易に検知され、ページの挙動を乱す可能性がある点に注意してください。この方法はテストや防御の目的により適しています。


// Robust script interception (best run at document_start)
(function() {
  if (window.__scriptInterceptorInstalled) return;
  window.__scriptInterceptorInstalled = true;

  // Your spoofing routine — define this to patch WebGL or other APIs.
  function injectSpoofing() {
    try {
      // Example: block debug info (replace with your real routine)
      const origGetExt = WebGLRenderingContext.prototype.getExtension;
      WebGLRenderingContext.prototype.getExtension = function(name) {
        if (name === 'WEBGL_debug_renderer_info') return null;
        return origGetExt.call(this, name);
      };
      // mark that spoofing ran
      window.__spoofingInjected = true;
    } catch (e) {
      // swallow errors — don't break page
      console.error('injectSpoofing error', e);
    }
  }

  // Helper: decide whether a script looks like fingerprinting (tweak to match)
  function looksLikeFingerprintScript(srcOrContent) {
    if (!srcOrContent) return false;
    const s = String(srcOrContent).toLowerCase();
    return s.includes('fingerprint') || s.includes('fingerprinting') || s.includes('fingerprintjs') || s.includes('fingerprint2');
  }

  // 1) Hook HTMLScriptElement.prototype.src property (setter)
  try {
    const proto = HTMLScriptElement.prototype;
    const srcDesc = Object.getOwnPropertyDescriptor(proto, 'src');
    if (srcDesc && srcDesc.configurable) {
      Object.defineProperty(proto, 'src', {
        configurable: true,
        enumerable: srcDesc.enumerable,
        get: function() { return srcDesc.get.call(this); },
        set: function(val) {
          try {
            // If the script's src looks suspicious, inject before setting
            if (looksLikeFingerprintScript(val) && !window.__spoofingInjected) {
              injectSpoofing();
            }
          } catch (e) { /* ignore */ }
          return srcDesc.set.call(this, val);
        }
      });
    }
  } catch (e) {
    // non-fatal
  }

  // 2) Hook setAttribute on script elements (covers element.setAttribute('src', ...))
  try {
    const origSetAttr = HTMLScriptElement.prototype.setAttribute;
    HTMLScriptElement.prototype.setAttribute = function(name, value) {
      try {
        if ((name + '').toLowerCase() === 'src' && looksLikeFingerprintScript(value) && !window.__spoofingInjected) {
          injectSpoofing();
        }
      } catch (e) {}
      return origSetAttr.call(this, name, value);
    };
  } catch (e) {}

  // 3) As a fallback, monitor append/insert to catch scripts that already had src set
  try {
    const origAppend = Node.prototype.appendChild;
    Node.prototype.appendChild = function(node) {
      try {
        if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
          const src = node.src || node.getAttribute && node.getAttribute('src');
          if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
            injectSpoofing();
          }
        }
      } catch (e) {}
      return origAppend.call(this, node);
    };

    const origInsertBefore = Node.prototype.insertBefore;
    Node.prototype.insertBefore = function(node, ref) {
      try {
        if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
          const src = node.src || node.getAttribute && node.getAttribute('src');
          if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
            injectSpoofing();
          }
        }
      } catch (e) {}
      return origInsertBefore.call(this, node, ref);
    };
  } catch (e) {}

  // 4) Small DOM observer as an extra safety net (lightweight)
  try {
    const obs = new MutationObserver((mutations) => {
      for (const m of mutations) {
        for (const n of m.addedNodes || []) {
          try {
            if (n && n.tagName && n.tagName.toLowerCase() === 'script') {
              const src = n.src || (n.getAttribute && n.getAttribute('src')) || n.textContent;
              if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
                injectSpoofing();
                obs.disconnect();
                return;
              }
            }
          } catch (e) {}
        }
      }
    });
    obs.observe(document, { childList: true, subtree: true });
  } catch (e) {}

  // If the page already has script tags at start, scan them now
  try {
    const scripts = document.getElementsByTagName('script');
    for (let i = 0; i < scripts.length; i++) {
      const s = scripts[i];
      const src = s.src || s.getAttribute && s.getAttribute('src') || s.textContent;
      if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
        injectSpoofing();
        break;
      }
    }
  } catch (e) {}
})();

      
        
        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
        
        
        
        
        
        
        

        
        
      

異なるページのWebGL環境は、互いの状態を参照できません。隔離されたコンテキストを作成することで、ウェブサイトによるページ間のフィンガープリント照合を防げます。

WebGLとCanvas

WebGLフィンガープリントとCanvasフィンガープリントはしばしば同じ意味で使われ、確かに両者は密接に関連しています。しかし、両者は100%同じではありません。

Canvasフィンガープリントは、ブラウザがHTML5 Canvasを使って2Dグラフィックスをどのようにレンダリングするかを分析します。レンダリング結果は主にブラウザのグラフィックススタックとレンダリングエンジンに依存します。そのため、スプーフィングは比較的容易です。

WebGLフィンガープリントは、ブラウザが2Dと3Dの両方のグラフィックスをどのようにレンダリングするかを分析します。表示結果はハードウェアの特性に依存するため、この方法ではより多くの情報が得られるため、回避もより困難です。

まとめ

WebGLフィンガープリントのスプーフィングの課題を知った今、10分前の何も知らなかった頃のほうがよかったと思うかもしれません。しかし、目標は見えなくなることではなく、ただ「普通のユーザー」に見えることです。そのためには、フィンガープリントが現実的に見え、セッション内で一貫しており、セッションごとに適切に変化している必要があります。そしてそれは十分に達成可能です。一方で、成功するスクレイピングは、あらゆる層で一貫性を保ち、マルチログインブラウザ, TLSスプーフィングソリューション, プロキシなどを活用したときに初めて可能になります。これらのツールとアプローチは相互に補完し合い、目立たずにアクセスしやすくします。 DataImpulseには、プライバシーと匿名性を守るためのサービスを提供しています。あらゆるスクレイピングのニーズに対応する、適法に取得されホワイトリストに登録されたプロキシと、中断のないデータ収集を支える24時間365日の有人サポートです。開始するには、「今すぐ試す」をクリックするか、[email protected]までお問い合わせください。

Share article: