{"record":{"id":"a0cf7da6b3ae5478","repo":"instructure/canvas-lms","slug":"zip-contents-exceed-course-quota-limit","errorCode":null,"errorMessage":"Zip contents exceed course quota limit","messagePattern":"Zip contents exceed course quota limit","errorType":"validation","errorClass":"Attachment::OverQuotaError","httpStatus":null,"severity":"error","filePath":"lib/unzip_attachment.rb","lineNumber":299,"sourceCode":"\n    # check whether the nominal size of the zip's contents would exceed\n    # quota, and reject the zip immediately if so\n    quota_hash = Attachment.get_quota(context)\n    if quota_hash[:quota] > 0\n      if (quota_hash[:quota_used] + total_size) > quota_hash[:quota]\n        raise Attachment::OverQuotaError, \"Zip file would exceed quota limit\"\n      end\n\n      @quota_remaining = quota_hash[:quota] - quota_hash[:quota_used]\n    end\n  end\n\n  # since the central directory can lie, track quota during extraction as well\n  # to prevent zip bomb denial-of-service attacks\n  def charge_quota(size)\n    return if @quota_remaining.nil?\n    if size > @quota_remaining\n      raise Attachment::OverQuotaError, \"Zip contents exceed course quota limit\"\n    end\n\n    @quota_remaining -= size\n  end\n\n  def paths_with_positions(base)\n    positions_hash = {}\n    paths.sort.each_with_index { |p, idx| positions_hash[p] = idx + base }\n    positions_hash\n  end\n\n  def percent_complete(current_index)\n    (current_index + 1).to_f / file_count.to_f\n  end\n\n  private\n\n  def process!","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/unzip_attachment.rb#L281-L317","documentation":"charge_quota in lib/unzip_attachment.rb:299 raises Attachment::OverQuotaError when an individual extracted entry's size exceeds the remaining quota tracked during extraction. Because zip central directories can lie about entry sizes, Canvas decrements @quota_remaining per entry to defend against zip-bomb denial-of-service attacks.","triggerScenarios":"Extraction encounters an entry whose actual inflated size exceeds @quota_remaining (quota was set by validate_against); a maliciously crafted or corrupted zip whose declared sizes are smaller than the real content.","commonSituations":"Malicious zip bombs uploaded by users; zips with inaccurate central directory metadata; legitimately large files uploaded when the course was just under quota per nominal size.","solutions":["Treat the upload as suspicious and verify the zip source; use a zip-bomb scanner for untrusted uploads","Free up quota in the target context and re-upload a smaller, verified archive","Delete partially extracted files from the failed attempt before retrying","Do not loosen this check — it is the anti-zip-bomb defense"],"exampleFix":"// before\n# zip declares 100KB entries but inflates to 5GB each\nUnzipAttachment.process(course, 'sneaky.zip')\n// after\n# pre-scan: reject zips with suspicious compression ratios or nested zips before processing","handlingStrategy":"try-catch","validationCode":"raise SuspiciousZip if zip_compression_ratio(zip_path) > MAX_SAFE_RATIO","typeGuard":null,"tryCatchPattern":"begin\n  UnzipAttachment.process(context, zip)\nrescue Attachment::OverQuotaError => e\n  logger.warn(\"possible zip bomb: #{e.message}\")\n  mark_upload_flagged(upload)\nend","preventionTips":["Never disable charge_quota — it blocks zip bombs","Pre-scan untrusted zips for nested archives and extreme ratios","Remove partially extracted artifacts after failure"],"tags":["ruby","zip","quota","zip-bomb","security"],"backgroundTag":"quota-exceeded","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"}