{"record":{"id":"314e5012b4e2fe3e","repo":"ankitects/anki","slug":"unexpected-api-access-please-report-this-message-on-the-anki","errorCode":null,"errorMessage":"Unexpected API access. Please report this message on the Anki forums.","messagePattern":"Unexpected API access\\. Please report this message on the Anki forums\\.","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"qt/aqt/mediasrv.py","lineNumber":1301,"sourceCode":"\n        return wrapped\n    else:\n        return NotFound(message=f\"{path} not found\")\n\n\ndef _check_dynamic_request_permissions():\n    if request.method == \"GET\":\n        return\n\n    def warn() -> None:\n        show_warning(\n            \"Unexpected API access. Please report this message on the Anki forums.\"\n        )\n\n    # check content type header to ensure this isn't an opaque request from another origin\n    if request.headers.get(\"Content-type\") != \"application/binary\":\n        aqt.mw.taskman.run_on_main(warn)\n        abort(403)\n\n    # does page have access to entire API?\n    if _have_api_access():\n        return\n\n    # whitelisted API endpoints for reviewer/previewer\n    if request.path in (\n        \"/_anki/getSchedulingStatesWithContext\",\n        \"/_anki/setSchedulingStates\",\n        \"/_anki/i18nResources\",\n        \"/_anki/congratsInfo\",\n    ):\n        pass\n    else:\n        # other legacy pages may contain third-party JS, so we do not\n        # allow them to access our API\n        aqt.mw.taskman.run_on_main(warn)\n        abort(403)","sourceCodeStart":1283,"sourceCodeEnd":1319,"githubUrl":"https://github.com/ankitects/anki/blob/2fae55543cfaa82880b84787b09b0ebf06ac9e29/qt/aqt/mediasrv.py#L1283-L1319","documentation":"_check_dynamic_request_permissions() in qt/aqt/mediasrv.py guards the local Anki API exposed over the media server. POST requests must come from pages Anki itself trusts: they need a `Content-type: application/binary` header (to prove they aren't opaque cross-origin form posts) and must either come from a page granted full API access (_have_api_access()) or hit a small whitelist of reviewer/previewer endpoints. When either check fails, Anki shows the warning 'Unexpected API access. Please report this message on the Anki forums.' on the main thread and aborts the request with HTTP 403. This is a deliberate CSRF/origin defense, not a bug in your collection.","triggerScenarios":"An HTTP POST to any /_anki endpoint whose `Content-type` header is not exactly `application/binary` (e.g. application/json, text/plain, missing, or the browser-normalized `application/x-www-form-urlencoded` from a cross-origin form), or a POST from a page without full API access (e.g. a legacy page or page embedding third-party JS) to an endpoint outside the whitelist (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo).","commonSituations":"Third-party JavaScript embedded in shared decks/add-on webviews trying to call the Anki backend API; add-on or script authors posting JSON instead of binary protobuf with the correct Content-type header; a malicious webpage on another origin submitting a form POST to localhost (the exact attack this guard blocks); custom tooling hitting the local media server without mimicking the official client headers.","solutions":["If you are the author of the code making the request, set the header `Content-type: application/binary` on POST requests to /_anki endpoints and post protobuf bytes, not JSON.","Only call whitelisted endpoints (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo) from reviewer/previewer contexts without full API access.","Use the official anki/tslib frontend code (which sends proper binary requests from trusted pages) instead of hand-rolled fetch calls against the local server.","If the warning appears during normal use of a stock deck/add-on, report it on the Anki forums with the steps to reproduce, as the message requests — it indicates untrusted JS attempted an API call."],"exampleFix":"// before\nfetch(\"/_anki/addNote\", {\n  method: \"POST\",\n  headers: { \"Content-type\": \"application/json\" },\n  body: JSON.stringify(payload),\n});\n\n// after\nfetch(\"/_anki/addNote\", {\n  method: \"POST\",\n  headers: { \"Content-type\": \"application/binary\" },\n  body: protobufBytes,\n});","handlingStrategy":"validation","validationCode":"const headers = { \"Content-type\": \"application/binary\" };\nconst allowed = [\"/_anki/getSchedulingStatesWithContext\", \"/_anki/setSchedulingStates\", \"/_anki/i18nResources\", \"/_anki/congratsInfo\"];\nif (!allowed.includes(path)) {\n  throw new Error(\"endpoint not permitted from this page context\");\n}\nawait fetch(path, { method: \"POST\", headers, body: protobufBytes });","typeGuard":"function isBinaryPost(init: RequestInit): boolean {\n  return new Headers(init.headers).get(\"Content-type\") === \"application/binary\";\n}","tryCatchPattern":"try {\n  const resp = await fetch(\"/_anki/\" + endpoint, { method: \"POST\", headers: { \"Content-type\": \"application/binary\" }, body: data });\n  if (resp.status === 403) throw new Error(\"API access denied for this page/endpoint\");\n} catch (e) {\n  console.error(\"Anki API call blocked:\", e);\n}","preventionTips":["Always POST to /_anki endpoints with Content-type exactly 'application/binary'.","From reviewer/previewer or third-party-embedding pages, restrict calls to the four whitelisted endpoints.","Do not embed untrusted third-party JS in pages that call the Anki backend API.","Treat a 403 plus the 'Unexpected API access' warning as a security guard firing, and fix the request origin/headers rather than bypassing it."],"tags":["csrf","http-403","security","content-type"],"backgroundTag":"permission-denied","analyzedSha":"2fae55543cfaa82880b84787b09b0ebf06ac9e29","analyzedAt":"2026-09-12T12:03:32.653Z","contentChangedAt":"2026-09-12T12:03:32.653Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}