{"record":{"id":"59de2a1c84ea2592","repo":"instructure/canvas-lms","slug":"attachment-verifier-token-id-mismatch-token-id-body-id","errorCode":null,"errorMessage":"Attachment verifier token id mismatch. token id: #{body[:id]}, attachment id: #{attachment.global_id}, token: #{verifier}","messagePattern":"Attachment verifier token id mismatch\\. token id: #(.+?), attachment id: #(.+?), token: #(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"app/models/attachments/verification.rb","lineNumber":80,"sourceCode":"    pm = opts[:permission_map_id]\n    body[:pm] = pm.to_s if pm && PERMISSION_MAPS.key?(pm)\n\n    CanvasSecurity.create_jwt(body, opts[:expires])\n  end\n\n  # Decodes a verifier and asserts its validity (but does not check permissions!). You\n  # probably want to use `valid_verifier_for_permission?`.\n  #\n  # @param verifier (String) - The verifier\n  #\n  # Returns nil if the verifier could not be decoded for whatever reason, and returns\n  # a Hash of the body contents if it can.\n  def decode_verifier(verifier)\n    begin\n      body = CanvasSecurity.decode_jwt(verifier)\n      if body[:id] != attachment.global_id\n        InstStatsd::Statsd.distributed_increment(\"attachments.token_verifier_id_mismatch\")\n        Rails.logger.warn(\"Attachment verifier token id mismatch. token id: #{body[:id]}, attachment id: #{attachment.global_id}, token: #{verifier}\")\n        return nil\n      end\n\n      InstStatsd::Statsd.distributed_increment(\"attachments.token_verifier_success\")\n    rescue CanvasSecurity::TokenExpired\n      InstStatsd::Statsd.distributed_increment(\"attachments.token_verifier_expired\")\n      Rails.logger.warn(\"Attachment verifier token expired: #{verifier}\")\n      return nil\n    rescue CanvasSecurity::InvalidToken\n      InstStatsd::Statsd.distributed_increment(\"attachments.token_verifier_invalid\")\n      Rails.logger.warn(\"Attachment verifier token invalid: #{verifier}\")\n      return nil\n    end\n\n    body\n  end\n\n  # Decodes a verifier and checks the user of the verifier has permission to access","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/attachments/verification.rb#L62-L98","documentation":"decode_verifier decodes an attachment verifier JWT and requires the token's :id claim to equal the attachment's global_id. On mismatch it logs this warning, emits the attachments.token_verifier_id_mismatch StatsD metric, and returns nil, so valid_verifier_for_permission? fails and access is denied. It is a defensive check against using a verifier issued for a different attachment.","triggerScenarios":"A client presents a verifier JWT (e.g. /files/:id/download?verifier=...) whose id claim points to a different attachment than the one being served — stale or hand-built URLs, copied links between files, or a token minted before an attachment was replaced.","commonSituations":"Bookmarked/cached download URLs reused after an attachment was recreated; course copies where the new attachment has a different global_id but an old verifier is replayed; manually constructed URLs reusing a verifier across files.","solutions":["Re-fetch the attachment and generate a fresh verifier via the standard download/inline URL flow instead of reusing the old token","Verify the URL's attachment id matches the id encoded in the verifier token (decode the JWT payload and compare :id)","If mismatches spike, audit code that builds file URLs manually and ensure it uses the attachment's own verifier","Check for proxies/caches serving stale signed URLs and disable caching of verifier-bearing URLs"],"exampleFix":"// before\nurl = \"/files/#{attachment.id}/download?verifier=#{old_verifier_from_other_file}\"\n// after\nurl = attachment.public_download_url # verifier minted for THIS attachment","handlingStrategy":"validation","validationCode":"def verifier_matches?(verifier, attachment)\n  payload = CanvasSecurity.decode_jwt(verifier) rescue nil\n  payload.is_a?(Hash) && payload[:id] == attachment.global_id\nend","typeGuard":"def valid_verifier_payload?(payload, attachment)\n  payload.is_a?(Hash) && payload[:id].to_i == attachment.global_id.to_i\nend","tryCatchPattern":"begin\n  body = CanvasSecurity.decode_jwt(verifier)\nrescue CanvasSecurity::TokenExpired, CanvasSecurity::InvalidToken => e\n  Rails.logger.warn(\"verifier rejected: #{e.class}\")\n  nil\nend","preventionTips":["Always generate file URLs through Canvas helpers, never hand-assemble verifier URLs","Never reuse a verifier across different attachments","Compare the token id claim with the attachment global_id before use","Disable caching of verifier-bearing URLs"],"tags":["jwt","attachments","authentication","security"],"backgroundTag":"jwt-token-expired","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}