{"uuid": "cab5d3e8-8053-4012-ab86-c8557de7ffe5", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2016-3714", "type": "seen", "source": "https://gist.github.com/Dnar/b176f53181102a3e489ea8964f889be5", "content": "File Upload Validation \u2014 Approach Comparison\n\n  :root {\n    --ground: #EDF1F8;\n    --surface: #FFFFFF;\n    --surface-alt: #E2E9F5;\n    --text: #0D1A30;\n    --text-muted: #556080;\n    --accent: #2454D4;\n    --accent-light: #EAF0FD;\n    --amber: #C87A00;\n    --amber-bg: #FDF5E4;\n    --amber-border: #E8C060;\n    --pro: #127A4E;\n    --pro-light: #E2F5EC;\n    --con: #C22E2E;\n    --con-light: #FCEAEA;\n    --border: #CED8EE;\n    --code-bg: #121E36;\n    --code-text: #B0C4E8;\n    --code-dim: #3A4F70;\n  }\n\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n  html { scroll-behavior: smooth; }\n\n  body {\n    font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Helvetica Neue\", Arial, sans-serif;\n    background: var(--ground);\n    color: var(--text);\n    font-size: 15px;\n    line-height: 1.65;\n    -webkit-font-smoothing: antialiased;\n  }\n\n  /* \u2500\u2500 HEADER \u2500\u2500 */\n  .hdr {\n    position: relative;\n    background: var(--code-bg);\n    color: #D0DCEF;\n    padding: 52px 32px 44px;\n    overflow: hidden;\n  }\n\n  #hx {\n    position: absolute;\n    inset: 0;\n    pointer-events: none;\n    opacity: 1;\n  }\n\n  .hdr-in {\n    position: relative;\n    z-index: 1;\n    max-width: 900px;\n    margin: 0 auto;\n  }\n\n  .eyebrow {\n    font-family: ui-monospace, \"SF Mono\", \"Menlo\", monospace;\n    font-size: 10.5px;\n    letter-spacing: 0.13em;\n    text-transform: uppercase;\n    color: var(--accent);\n    background: rgba(36, 84, 212, 0.15);\n    border: 1px solid rgba(36, 84, 212, 0.3);\n    display: inline-block;\n    padding: 3px 10px;\n    border-radius: 4px;\n    margin-bottom: 18px;\n  }\n\n  .hdr h1 {\n    font-size: clamp(26px, 4vw, 42px);\n    font-weight: 800;\n    letter-spacing: -0.035em;\n    line-height: 1.12;\n    margin-bottom: 14px;\n  }\n\n  .hdr h1 em {\n    font-style: normal;\n    color: #F0B030;\n  }\n\n  .hdr-meta {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 20px;\n    margin-top: 20px;\n  }\n\n  .hdr-meta-item {\n    font-family: ui-monospace, monospace;\n    font-size: 11.5px;\n    color: #5A7AAA;\n  }\n\n  .hdr-meta-item strong {\n    color: #8AAAD0;\n    font-weight: 500;\n    margin-right: 6px;\n  }\n\n  /* \u2500\u2500 MAIN \u2500\u2500 */\n  main {\n    max-width: 940px;\n    margin: 0 auto;\n    padding: 36px 24px 80px;\n  }\n\n  /* \u2500\u2500 ALERT CARD \u2500\u2500 */\n  .alert {\n    background: var(--amber-bg);\n    border: 1px solid var(--amber-border);\n    border-left: 4px solid var(--amber);\n    border-radius: 8px;\n    padding: 18px 22px;\n    margin-bottom: 40px;\n  }\n\n  .alert-label {\n    font-family: ui-monospace, monospace;\n    font-size: 10px;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n    color: var(--amber);\n    font-weight: 700;\n    margin-bottom: 8px;\n  }\n\n  .alert p {\n    font-size: 14px;\n    color: #5A3800;\n    line-height: 1.65;\n  }\n\n  .alert code {\n    font-family: ui-monospace, monospace;\n    font-size: 12px;\n    background: rgba(200, 122, 0, 0.1);\n    color: #7A4E00;\n    padding: 1px 5px;\n    border-radius: 3px;\n  }\n\n  /* \u2500\u2500 SECTION LABEL \u2500\u2500 */\n  .section-lbl {\n    font-family: ui-monospace, monospace;\n    font-size: 10.5px;\n    letter-spacing: 0.12em;\n    text-transform: uppercase;\n    color: var(--text-muted);\n    border-bottom: 1px solid var(--border);\n    padding-bottom: 10px;\n    margin-bottom: 20px;\n    margin-top: 40px;\n  }\n\n  /* \u2500\u2500 COMPARISON GRID \u2500\u2500 */\n  .cmp-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 18px;\n    margin-bottom: 48px;\n  }\n\n  @media (max-width: 620px) { .cmp-grid { grid-template-columns: 1fr; } }\n\n  .cmp-card {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: 10px;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n  }\n\n  .cmp-card-hdr {\n    padding: 20px 22px 16px;\n    border-bottom: 1px solid var(--border);\n    background: var(--ground);\n  }\n\n  .badge {\n    font-family: ui-monospace, monospace;\n    font-size: 9.5px;\n    letter-spacing: 0.1em;\n    text-transform: uppercase;\n    font-weight: 700;\n    padding: 2px 7px;\n    border-radius: 3px;\n    margin-bottom: 10px;\n    display: inline-block;\n  }\n\n  .badge-current { background: var(--surface-alt); color: var(--text-muted); }\n  .badge-proposed { background: var(--accent-light); color: var(--accent); }\n\n  .cmp-card h3 {\n    font-size: 17px;\n    font-weight: 750;\n    letter-spacing: -0.02em;\n    line-height: 1.25;\n    margin-bottom: 8px;\n  }\n\n  .how-it-works {\n    font-size: 13px;\n    color: var(--text-muted);\n    line-height: 1.55;\n  }\n\n  .hex-tags {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 5px;\n    margin-top: 10px;\n  }\n\n  .hex-tag {\n    font-family: ui-monospace, monospace;\n    font-size: 10px;\n    background: var(--code-bg);\n    color: #7AA0D0;\n    padding: 2px 7px;\n    border-radius: 3px;\n    letter-spacing: 0.08em;\n  }\n\n  .cmp-card-body {\n    padding: 20px 22px;\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 18px;\n  }\n\n  .pc-block-lbl {\n    font-family: ui-monospace, monospace;\n    font-size: 10px;\n    font-weight: 700;\n    letter-spacing: 0.1em;\n    text-transform: uppercase;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    margin-bottom: 10px;\n  }\n\n  .pc-block-lbl.pro { color: var(--pro); }\n  .pc-block-lbl.con { color: var(--con); }\n\n  .pc-block-lbl::before {\n    content: '';\n    width: 5px;\n    height: 5px;\n    border-radius: 50%;\n    flex-shrink: 0;\n    display: block;\n  }\n\n  .pc-block-lbl.pro::before { background: var(--pro); }\n  .pc-block-lbl.con::before { background: var(--con); }\n\n  .pc-list {\n    list-style: none;\n    display: flex;\n    flex-direction: column;\n    gap: 7px;\n  }\n\n  .pc-list li {\n    font-size: 13.5px;\n    line-height: 1.5;\n    display: flex;\n    gap: 8px;\n    align-items: flex-start;\n  }\n\n  .pc-list li .mk {\n    font-family: ui-monospace, monospace;\n    font-size: 11px;\n    font-weight: 700;\n    flex-shrink: 0;\n    margin-top: 2px;\n    width: 14px;\n  }\n\n  .pc-list.pro-list .mk { color: var(--pro); }\n  .pc-list.con-list .mk { color: var(--con); }\n\n  .crit {\n    font-family: ui-monospace, monospace;\n    font-size: 9px;\n    font-weight: 700;\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    background: var(--con-light);\n    color: var(--con);\n    padding: 1px 5px;\n    border-radius: 3px;\n    vertical-align: middle;\n    margin-left: 4px;\n  }\n\n  /* \u2500\u2500 RECOMMENDATION \u2500\u2500 */\n  .rec {\n    background: var(--surface);\n    border: 2px solid var(--accent);\n    border-radius: 12px;\n    overflow: hidden;\n    margin-bottom: 40px;\n  }\n\n  .rec-hdr {\n    background: var(--accent);\n    color: white;\n    padding: 14px 22px;\n    display: flex;\n    align-items: center;\n    gap: 10px;\n  }\n\n  .rec-hdr h2 {\n    font-size: 14px;\n    font-weight: 700;\n    letter-spacing: 0.01em;\n  }\n\n  .rec-icon { font-size: 18px; }\n\n  .rec-body {\n    padding: 22px;\n  }\n\n  .rec-body p {\n    font-size: 14px;\n    line-height: 1.7;\n    color: var(--text);\n    margin-bottom: 12px;\n  }\n\n  .rec-body p:last-child { margin-bottom: 0; }\n\n  /* \u2500\u2500 IMPROVEMENTS \u2500\u2500 */\n  .improve-list {\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n    margin-bottom: 40px;\n  }\n\n  .improve-item {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: 8px;\n    padding: 14px 18px;\n    display: flex;\n    gap: 14px;\n    align-items: flex-start;\n  }\n\n  .improve-num {\n    font-family: ui-monospace, monospace;\n    font-size: 12px;\n    font-weight: 700;\n    color: var(--accent);\n    background: var(--accent-light);\n    width: 28px;\n    height: 28px;\n    border-radius: 6px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n  }\n\n  .improve-text strong {\n    display: block;\n    font-size: 14px;\n    font-weight: 650;\n    letter-spacing: -0.01em;\n    margin-bottom: 3px;\n  }\n\n  .improve-text p {\n    font-size: 13px;\n    color: var(--text-muted);\n    line-height: 1.55;\n  }\n\n  .improve-text code {\n    font-family: ui-monospace, monospace;\n    font-size: 11.5px;\n    background: var(--surface-alt);\n    padding: 1px 5px;\n    border-radius: 3px;\n    color: var(--text);\n  }\n\n  /* \u2500\u2500 CODE BLOCK \u2500\u2500 */\n  .cb {\n    background: var(--code-bg);\n    border-radius: 10px;\n    overflow: hidden;\n    margin-bottom: 48px;\n  }\n\n  .cb-hdr {\n    background: #0C1628;\n    padding: 10px 18px;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .cb-dots { display: flex; gap: 5px; }\n  .cb-dot { width: 9px; height: 9px; border-radius: 50%; background: #2A3A54; }\n\n  .cb-fname {\n    font-family: ui-monospace, monospace;\n    font-size: 11.5px;\n    color: #4A6888;\n  }\n\n  .cb pre {\n    padding: 22px 24px;\n    overflow-x: auto;\n    font-family: ui-monospace, \"SF Mono\", \"Menlo\", monospace;\n    font-size: 13px;\n    line-height: 1.75;\n    color: var(--code-text);\n    tab-size: 2;\n  }\n\n  .kw { color: #7A9EE8; }\n  .cm { color: #3E5A7A; font-style: italic; }\n  .st { color: #88C4A0; }\n  .cn { color: #E0B860; }\n  .nm { color: #B0C4E8; }\n  .mt { color: #78AACC; }\n\n  /* \u2500\u2500 FOOTER \u2500\u2500 */\n  footer {\n    max-width: 940px;\n    margin: 0 auto;\n    padding: 0 24px 48px;\n    border-top: 1px solid var(--border);\n    padding-top: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 16px;\n    flex-wrap: wrap;\n  }\n\n  footer span {\n    font-family: ui-monospace, monospace;\n    font-size: 11.5px;\n    color: var(--text-muted);\n  }\n\n  .verdict-pill {\n    font-family: ui-monospace, monospace;\n    font-size: 11px;\n    font-weight: 700;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n    background: var(--pro-light);\n    color: var(--pro);\n    border: 1px solid #A0D8BC;\n    padding: 4px 12px;\n    border-radius: 20px;\n  }\n\n\n\n\n  \n  \n\n    \nCARE-2255 \u00b7 Pentest Finding\n    \nFile Upload Validation:Magic Bytes vs. MiniMagick\n    \n\n      \nVulnerability MIME-type spoofing on file upload\n      \nBranch CARE-2255-upload-files-magic-bytes-validator\n      \nScope Customer::Attachment \u00b7 Claim::Document::Attachment\n    \n  \n\n\n\n\n\n  \n\n    \nPentest Finding \u2014 Context\n    \n\n      An attacker can rename a file with a dangerous extension (e.g. a PHP shell saved as\n      exploit.jpg) and upload it with a spoofed Content-Type: image/jpeg header.\n      The app currently validates only the browser-declared MIME type and file size \u2014\n      both are trivially forged. The fix must inspect the file's actual binary content\n      before accepting it.\n    \n  \n\n  \nApproach comparison\n\n  \n\n\n    \n    \n\n      \n\n        \nCARE-2255 Implementation\n        \nCustom Magic Bytes Validator\n        \n\n          Reads the first 4 bytes from the file and compares them to a hardcoded\n          lookup table of known binary signatures. Pure Ruby, no system calls.\n        \n        \n\n          PNG: 89 50 4E 47\n          JPEG: FF D8 FF E0\n          PDF: 25 50 44 46\n          TIFF: 49 49 2A 00\n        \n      \n      \n\n        \n\n          \nPros\n          \n\n            \n+No new dependencies \u2014 standard Ruby library only\n            \n+Extremely fast \u2014 reads 4 bytes, no process spawn\n            \n+Fully transparent and auditable \u2014 all logic lives in our codebase\n            \n+Minimal attack surface \u2014 the file is never parsed, rendered, or executed\n            \n+Safe against parser exploits \u2014 a malicious file cannot trigger library vulnerabilities\n            \n+Easy to extend \u2014 adding a new format is one hash entry\n          \n        \n        \n\n          \nCons\n          \n\n            \n\u2212Header-only check \u2014 a polyglot file (valid magic bytes + malicious payload) still passes\n            \n\u2212Incomplete JPEG coverage \u2014 only handles FF D8 FF E0, misses Exif (FF D8 FF E1) and other JPEG variants\n            \n\u2212No structural integrity check \u2014 corrupt or degenerate files pass through\n            \n\u2212Validation was only in the domain service, not enforced at the model layer\n          \n        \n      \n    \n\n    \n    \n\n      \n\n        \nSuggested by Sergii Gorobets\n        \nMiniMagick (ImageMagick)\n        \n\n          Calls ImageMagick via MiniMagick::Image.open(path)\n          to parse and identify the file. Already in Gemfile at version 4.12.0.\n        \n        \n\n          mini_magick 4.12.0\n          ImageMagick / identify\n        \n      \n      \n\n        \n\n          \nPros\n          \n\n            \n+Deeper validation \u2014 ImageMagick parses the full file structure, not just the header\n            \n+Already a dependency \u2014 mini_magick is in Gemfile.lock, no new gem needed\n            \n+Handles all JPEG variants correctly out of the box\n            \n+Detects corrupt or degenerate image files\n          \n        \n        \n\n          \nCons\n          \n\n            \n\u2212ImageMagick RCE historyCritical \u2014 ImageTragick (CVE-2016-3714), CVE-2022-44268, and multiple others. Processing untrusted uploads with ImageMagick is itself an attack vector.\n            \n\u2212Requires hardened policy.xml to mitigate risks \u2014 operationally complex and easy to misconfigure\n            \n\u2212PDFs not supportedGap \u2014 ImageMagick needs Ghostscript for PDF processing, adding another attack surface and system dependency\n            \n\u2212External process per upload \u2014 higher latency, more resource consumption\n            \n\u2212Over-engineered for the goal \u2014 we need to prevent MIME spoofing, not validate full image integrity\n          \n        \n      \n    \n\n  \n\n  \n  \n\n    \n\n      &#10003;\n      \nRecommendation \u2014 Improve the Custom Magic Bytes Approach\n    \n    \n\n      \n\n        The pentest finding is specifically about MIME-type spoofing: a file with a falsely declared\n        Content-Type\n        header. The CARE-2255 approach directly addresses this with minimal complexity and no additional attack surface.\n      \n      \n\n        MiniMagick introduces a more serious risk than it solves: running ImageMagick on\n        untrusted user input is the root cause of an entire class of server-side\n        vulnerabilities. For PDF validation specifically, it would require Ghostscript \u2014 compounding both\n        the dependency footprint and the exposure. The \"deeper validation\" benefit is irrelevant\n        to the threat model here.\n      \n      \n\n        Decision: do not adopt MiniMagick for file validation.\n        Instead, fix the known weaknesses in the existing magic bytes approach and move validation\n        to the model layer so it applies universally.\n      \n    \n  \n\n  \nImprovements to the chosen approach\n\n  \n\n\n    \n\n      \n1\n      \n\n        Extend JPEG magic byte coverage\n        \nAdd the missing JPEG variants: FF D8 FF E1 (Exif), FF D8 FF E2, FF D8 FF DB (JFIF without marker). The current implementation only covers FF D8 FF E0 and would reject legitimate Exif-encoded photos from modern cameras.\n      \n    \n\n    \n\n      \n2\n      \n\n        Move validation to the model layer\n        \nAdd a custom ActiveModel::Validator on Customer::Attachment so magic bytes are checked regardless of the call path. Currently, validation only runs inside Customers::AttachFiles \u2014 bypassing it is trivial if files are attached from any other context.\n      \n    \n\n    \n\n      \n3\n      \n\n        Apply to Claim::Document::Attachment\n        \nThis model only validates content_type (browser-declared) and size. It is fully exposed to the same spoofing attack. The magic bytes validator must also be applied here.\n      \n    \n\n    \n\n      \n4\n      \n\n        Log mismatches to Datadog\n        \nWhen magic bytes do not match the declared MIME type, emit a structured log entry. Spoofed uploads are either an automated probe or an active attack \u2014 both are worth detecting and alerting on.\n      \n    \n\n    \n\n      \n5\n      \n\n        Validate extension\u2013MIME consistency\n        \nAs a second layer, check that the file extension in original_filename is consistent with the declared content_type. This catches mismatches before even reading the file and adds defense-in-depth at no cost.\n      \n    \n\n  \n\n  \nImproved implementation sketch\n\n  \n\n    \n\n      \n\n        \n\n        \n\n        \n\n      \n      app/services/file_validator.rb\n    \n    \n# frozen_string_literal: true\n\nclass FileValidator\n  MAGIC_BYTES = {\n    \"png\"  =&gt; [\"\\x89PNG\".bytes],\n    \"jpg\"  =&gt; [\"\\xFF\\xD8\\xFF\\xE0\".bytes, \"\\xFF\\xD8\\xFF\\xE1\".bytes,\n                \"\\xFF\\xD8\\xFF\\xE2\".bytes, \"\\xFF\\xD8\\xFF\\xDB\".bytes],\n    \"jpeg\" =&gt; [\"\\xFF\\xD8\\xFF\\xE0\".bytes, \"\\xFF\\xD8\\xFF\\xE1\".bytes,\n                \"\\xFF\\xD8\\xFF\\xE2\".bytes, \"\\xFF\\xD8\\xFF\\xDB\".bytes],\n    \"pdf\"  =&gt; [\"\\x25\\x50\\x44\\x46\".bytes],\n    \"tif\"  =&gt; [\"\\x49\\x49\\x2A\\x00\".bytes, \"\\x4D\\x4D\\x00\\x2A\".bytes],\n    \"tiff\" =&gt; [\"\\x49\\x49\\x2A\\x00\".bytes, \"\\x4D\\x4D\\x00\\x2A\".bytes]\n  }.freeze\n\n  def call(file)\n    file_bytes = File.read(file.path, 4).bytes\n    ext = file.original_filename.split(\".\").last&amp;.downcase\n\n    signatures = MAGIC_BYTES[ext]\n    return false if signatures.nil? # unsupported extension\n\n    valid = signatures.any? { |sig| file_bytes == sig }\n    log_mismatch(file, ext) unless valid\n    valid\n  end\n\n  private\n\n  def log_mismatch(file, ext)\n    Rails.logger.warn({\n      event: \"magic_bytes_mismatch\",\n      filename: file.original_filename,\n      declared_ext: ext,\n      content_type: file.content_type\n    }.to_json)\n  end\nend\n  \n\n\n\n\n\n  CARE-2255 \u00b7 File Upload Security \u00b7 2026\n  \nImprove custom validator\n\n\n\n(function () {\n  const canvas = document.getElementById('hx');\n  const ctx = canvas.getContext('2d');\n\n  const SEQUENCES = [\n    ['89','50','4E','47','0D','0A','1A','0A'],\n    ['FF','D8','FF','E0','00','10','4A','46'],\n    ['FF','D8','FF','E1','00','18','45','78'],\n    ['25','50','44','46','2D','31','2E','34'],\n    ['49','49','2A','00','08','00','00','00'],\n    ['4D','4D','00','2A','00','00','00','08'],\n  ];\n\n  let cells = [], cols, rows;\n  let rafId = null;\n  let lastTick = 0;\n\n  const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n  function rndHex() {\n    return Math.floor(Math.random() * 256).toString(16).toUpperCase().padStart(2, '0');\n  }\n\n  function init() {\n    canvas.width = canvas.offsetWidth;\n    canvas.height = canvas.offsetHeight;\n    const cellW = 34, cellH = 18;\n    cols = Math.ceil(canvas.width / cellW) + 1;\n    rows = Math.ceil(canvas.height / cellH) + 1;\n    cells = [];\n    for (let r = 0; r &lt; rows; r++) {\n      cells[r] = [];\n      for (let c = 0; c &lt; cols; c++) {\n        cells[r][c] = {\n          val: rndHex(),\n          op: Math.random() * 0.18 + 0.04,\n          hi: false,\n          decay: 0\n        };\n      }\n    }\n    // Embed magic sequences across rows\n    SEQUENCES.forEach(function(seq, si) {\n      const r = 1 + si * Math.floor((rows - 2) / SEQUENCES.length);\n      const startCol = Math.floor(Math.random() * Math.max(1, cols - seq.length - 4));\n      seq.forEach(function(b, ci) {\n        if (r &lt; rows &amp;&amp; (startCol + ci) &lt; cols) {\n          cells[r][startCol + ci] = { val: b, op: 0.55, hi: true, decay: 0 };\n        }\n      });\n    });\n\n    if (reduced) {\n      draw();\n    } else {\n      if (rafId) cancelAnimationFrame(rafId);\n      rafId = requestAnimationFrame(tick);\n    }\n  }\n\n  function tick(ts) {\n    rafId = requestAnimationFrame(tick);\n    if (ts - lastTick &lt; 120) return;\n    lastTick = ts;\n\n    // Randomly update a few non-highlight cells\n    for (let i = 0; i &lt; 6; i++) {\n      const r = Math.floor(Math.random() * rows);\n      const c = Math.floor(Math.random() * cols);\n      const cell = cells[r][c];\n      if (!cell.hi) {\n        cell.val = rndHex();\n        cell.op = Math.random() * 0.18 + 0.04;\n      }\n    }\n\n    draw();\n  }\n\n  function draw() {\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    ctx.font = '10px ui-monospace, SF Mono, Menlo, monospace';\n    const cellW = 34, cellH = 18;\n    for (let r = 0; r &lt; rows; r++) {\n      for (let c = 0; c &lt; cols; c++) {\n        const cell = cells[r][c];\n        if (cell.hi) {\n          ctx.fillStyle = 'rgba(100, 160, 255, ' + cell.op + ')';\n        } else {\n          ctx.fillStyle = 'rgba(160, 190, 230, ' + cell.op + ')';\n        }\n        ctx.fillText(cell.val, c * cellW + 3, r * cellH + 13);\n      }\n    }\n  }\n\n  init();\n  window.addEventListener('resize', function() {\n    if (rafId) cancelAnimationFrame(rafId);\n    init();\n  });\n})();\n\n", "creation_timestamp": "2026-06-24T14:28:16.000000Z"}