{"record":{"id":"d0d477cae94c6eb8","repo":"actualbudget/actual","slug":"invalid-font-src-only-data-uris-are-allowed-in","errorCode":null,"errorMessage":"Invalid font src: only data: URIs are allowed in @font-face. Remote URLs (http/https) are not permitted to protect user privacy. Font files are automatically embedded when installing from GitHub.","messagePattern":"Invalid font src: only data: URIs are allowed in @font-face\\. Remote URLs \\(http/https\\) are not permitted to protect user privacy\\. Font files are automatically embedded when installing from GitHub\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/style/customThemes.ts","lineNumber":272,"sourceCode":"}\n\n/**\n * Validate @font-face blocks: only data: URIs allowed (no remote URLs).\n * Enforces size limits to prevent DoS.\n */\nfunction validateFontFaceBlocks(fontFaceBlocks: string[]): void {\n  let totalSize = 0;\n  // Match url() with quoted or unquoted content. Quoted URLs use a non-greedy\n  // match up to the closing quote; unquoted URLs match non-whitespace/non-paren.\n  const urlRegex = /url\\(\\s*(?:'([^']*)'|\"([^\"]*)\"|([^'\")\\s]+))\\s*\\)/g;\n\n  for (const block of fontFaceBlocks) {\n    urlRegex.lastIndex = 0;\n    let match;\n    while ((match = urlRegex.exec(block)) !== null) {\n      const uri = (match[1] ?? match[2] ?? match[3]).trim();\n      if (!uri.startsWith('data:')) {\n        throw new Error(\n          'Invalid font src: only data: URIs are allowed in @font-face. ' +\n            'Remote URLs (http/https) are not permitted to protect user privacy. ' +\n            'Font files are automatically embedded when installing from GitHub.',\n        );\n      }\n      // Estimate decoded size from base64 content\n      const base64Match = uri.match(/;base64,(.+)$/);\n      if (base64Match) {\n        const size = Math.ceil((base64Match[1].length * 3) / 4);\n        if (size > MAX_FONT_FILE_SIZE) {\n          throw new Error(\n            `Font file exceeds maximum size of ${MAX_FONT_FILE_SIZE / 1024 / 1024}MB.`,\n          );\n        }\n        totalSize += size;\n      }\n    }\n  }","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/style/customThemes.ts#L254-L290","documentation":"validateFontFaceBlocks scans @font-face blocks in theme CSS and requires every src url() to be a data: URI. Remote http/https font URLs are blocked to protect user privacy (they would leak requests to third parties); themes installed from GitHub are expected to have fonts embedded as base64 data URIs automatically.","triggerScenarios":"A theme's actual.css contains `@font-face { src: url(https://fonts.example.com/x.woff2); }` or a relative/protocol-relative URL — anything not starting with 'data:' inside @font-face.","commonSituations":"Theme author linked Google Fonts or a CDN font directly instead of embedding it; a font-embedding script failed to inline the font file; hand-written @font-face pointing at a local file path.","solutions":["Embed the font as a base64 data URI: `src: url(data:font/woff2;base64,<...>) format('woff2');`.","Re-install the theme through the GitHub flow so fonts are automatically embedded at install time.","Drop the @font-face block and rely on locally installed fonts named in --font-* variables.","Use a build step (e.g. a script that inlines woff2 files as data URIs) before publishing actual.css."],"exampleFix":"// before\n@font-face { font-family: 'Inter'; src: url('https://cdn.example.com/inter.woff2'); }\n// after\n@font-face { font-family: 'Inter'; src: url('data:font/woff2;base64,d09GMgAB...') format('woff2'); }","handlingStrategy":"validation","validationCode":"function fontSrcsAreDataUris(css) {\n  const blocks = css.match(/@font-face\\s*{[^}]*}/g) ?? [];\n  return blocks.every(b => (b.match(/url\\((['\"]?)([^)\"']+)(\\1)\\)/g) ?? [])\n    .every(u => u.includes('url(data:')));\n}\nif (!fontSrcsAreDataUrs(css)) throw new Error('remote font URL found');","typeGuard":"function isDataUri(uri: string): boolean {\n  return uri.startsWith('data:');\n}","tryCatchPattern":"try {\n  await installTheme(css);\n} catch (err) {\n  if ((err as Error).message.includes('only data: URIs are allowed')) {\n    // instruct author to embed fonts as base64 or reinstall via the GitHub flow\n  } else throw err;\n}","preventionTips":["Always inline fonts as base64 data URIs in theme CSS.","Use the GitHub install flow, which embeds fonts automatically.","Grep published actual.css for 'url(http' before release."],"tags":["css","security","privacy","font-face","validation"],"backgroundTag":"remote-resource-not-allowed","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}