mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 06:03:01 -05:00
[AI] Allow var(--name) in custom theme CSS (no fallbacks) (#7018)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
committed by
GitHub
parent
5179ac7c2d
commit
77b848ca84
@@ -457,30 +457,18 @@ describe('validateThemeCss', () => {
|
||||
});
|
||||
|
||||
describe('invalid CSS - function calls (security)', () => {
|
||||
describe('var() function - should reject all var() references', () => {
|
||||
describe('var() function - should accept var(--name) only (no fallbacks)', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'simple var() call',
|
||||
css: `:root {
|
||||
--color-primary: var(--other-var);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'var() with fallback',
|
||||
css: `:root {
|
||||
--color-primary: var(--other-var, #007bff);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'var() with whitespace',
|
||||
css: `:root {
|
||||
--color-primary: var( --other-var );
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'nested var() calls',
|
||||
css: `:root {
|
||||
--color-primary: var(--var1, var(--var2));
|
||||
}`,
|
||||
},
|
||||
{
|
||||
@@ -495,14 +483,43 @@ describe('validateThemeCss', () => {
|
||||
--color-primary: VaR(--other-var);
|
||||
}`,
|
||||
},
|
||||
])('should reject CSS with $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow(
|
||||
/Only simple CSS values are allowed/,
|
||||
);
|
||||
])('should accept CSS with $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('other function calls - should reject all except rgb/rgba/hsl/hsla', () => {
|
||||
describe('var() function - should reject var() with fallback or invalid form', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'var() with fallback',
|
||||
css: `:root {
|
||||
--color-primary: var(--other-var, #007bff);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'var() with invalid variable name (no --)',
|
||||
css: `:root {
|
||||
--color-primary: var(not-a-custom-prop);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'var() with empty variable name',
|
||||
css: `:root {
|
||||
--color-primary: var(--);
|
||||
}`,
|
||||
},
|
||||
{
|
||||
description: 'var() with unbalanced parentheses',
|
||||
css: `:root {
|
||||
--color-primary: var(--other-var;
|
||||
}`,
|
||||
},
|
||||
])('should reject CSS with $description', ({ css }) => {
|
||||
expect(() => validateThemeCss(css)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('other function calls - should reject all except rgb/rgba/hsl/hsla and var()', () => {
|
||||
it.each([
|
||||
{
|
||||
description: 'calc() function',
|
||||
|
||||
@@ -73,10 +73,19 @@ export async function fetchDirectCss(url: string): Promise<string> {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
/** Only var(--custom-property-name) is allowed; no fallbacks. Variable name: -- then [a-zA-Z0-9_-]+ (no trailing dash). */
|
||||
const VAR_ONLY_PATTERN = /^var\s*\(\s*(--[a-zA-Z0-9_-]+)\s*\)$/i;
|
||||
|
||||
function isValidSimpleVarValue(value: string): boolean {
|
||||
const m = value.trim().match(VAR_ONLY_PATTERN);
|
||||
if (!m) return false;
|
||||
const name = m[1];
|
||||
return name !== '--' && !name.endsWith('-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a CSS property value only contains allowed content (allowlist approach).
|
||||
* Only simple, safe CSS values are allowed - no functions (except rgb/rgba/hsl/hsla), no URLs, no complex constructs.
|
||||
* Explicitly rejects var() and other function calls to prevent variable references and complex expressions.
|
||||
* Allows: colors (hex, rgb/rgba, hsl/hsla), lengths, numbers, keywords, and var(--name) only (no fallbacks).
|
||||
*/
|
||||
function validatePropertyValue(value: string, property: string): void {
|
||||
if (!value || value.length === 0) {
|
||||
@@ -85,12 +94,15 @@ function validatePropertyValue(value: string, property: string): void {
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (isValidSimpleVarValue(trimmedValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allowlist: Only allow specific safe CSS value patterns
|
||||
// 1. Hex colors: #RGB, #RRGGBB, or #RRGGBBAA (3, 6, or 8 hex digits)
|
||||
const hexColorPattern = /^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?([0-9a-fA-F]{2})?$/;
|
||||
|
||||
// 2. RGB/RGBA functions: rgb(...) or rgba(...) with simple numeric/percentage values
|
||||
// Allow optional whitespace and support both integers and decimals
|
||||
const rgbRgbaPattern =
|
||||
/^rgba?\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*(,\s*[\d.]+)?\s*\)$/;
|
||||
|
||||
@@ -123,7 +135,7 @@ function validatePropertyValue(value: string, property: string): void {
|
||||
|
||||
// If none of the allowlist patterns match, reject the value
|
||||
throw new Error(
|
||||
`Invalid value "${trimmedValue}" for property "${property}". Only simple CSS values are allowed (colors, lengths, numbers, or keywords). Functions (including var()), URLs, and other complex constructs are not permitted.`,
|
||||
`Invalid value "${trimmedValue}" for property "${property}". Only simple CSS values are allowed (colors, lengths, numbers, keywords, or var(--name)). Other functions, URLs, and complex constructs are not permitted.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,21 @@ Custom themes must be written as CSS using the `:root` selector with CSS custom
|
||||
|
||||
- The CSS must contain **exactly** `:root { ... }` and nothing else
|
||||
- Only custom properties starting with `--` are allowed; Actual uses `--color-*` variables for theming
|
||||
- **Values** may use `var(--custom-property-name)` to reference other variables (e.g. to reuse existing theme variables). Only this form is allowed; fallbacks like `var(--name, value)` are not supported.
|
||||
- No other selectors, at-rules (@import, @media, etc.), or nested blocks are allowed
|
||||
- Comments are allowed and will be stripped during validation
|
||||
|
||||
Example using variables:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-pageBackground: #1a1a1a;
|
||||
--color-pageText: #ffffff;
|
||||
/* Reuse another variable */
|
||||
--color-buttonPrimaryBackground: var(--color-pageTextLink);
|
||||
}
|
||||
```
|
||||
|
||||
### Available CSS Variables
|
||||
|
||||
Custom themes can override any of the CSS variables defined in Actual's base themes. These variables correspond to the theme color keys found in the theme files:
|
||||
@@ -125,9 +137,10 @@ When you paste CSS or install from a catalog, the theme is validated to ensure i
|
||||
|
||||
1. Must contain exactly `:root { ... }`
|
||||
2. Only custom properties starting with `--` are allowed; Actual uses `--color-*` variables for theming
|
||||
3. No at-rules (@import, @media, @keyframes, etc.)
|
||||
4. No nested selectors or blocks
|
||||
5. No content outside the `:root` block
|
||||
3. Values may be literals (colors, lengths, etc.) or `var(--name)` references (no fallbacks)
|
||||
4. No at-rules (@import, @media, @keyframes, etc.)
|
||||
5. No nested selectors or blocks
|
||||
6. No content outside the `:root` block
|
||||
|
||||
If validation fails, you'll see an error message explaining what's wrong.
|
||||
|
||||
|
||||
6
upcoming-release-notes/7018.md
Normal file
6
upcoming-release-notes/7018.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Custom themes: allow using simple CSS variables
|
||||
Reference in New Issue
Block a user