Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,40 +36,86 @@
<!-- Knox Theme Loader (Inline & Blocking to prevent FOUC) -->
<script type="text/javascript">
(function() {
// Theme names are attacker-influenced (URL parameter and localStorage), so
// every candidate is validated before it is stored or used to build a URL.
// Rejecting quotes, angle brackets, dots and path separators means the only
// URL this can ever produce is styles/themes/<name>/theme.css - so the set
// of themes actually installed on the server is the effective allowlist,
// and an unknown name simply 404s and leaves the base styles in place.
var THEME_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;

function sanitize(candidate) {
return (candidate && THEME_PATTERN.test(candidate)) ? candidate : null;
}

// Load theme based on: deployment lock > URL parameter > localStorage > deployment default
var urlParams = new URLSearchParams(window.location.search);
var theme;
// True when the theme about to load is also the value held in localStorage,
// which is what makes it eligible for cleanup if the stylesheet fails.
var themeIsPersisted = false;

// Check if theme is locked (admin-enforced theme)
if (typeof KNOX_THEME_LOCKED !== 'undefined' && KNOX_THEME_LOCKED === true) {
// Theme is locked, use deployment default only
theme = KNOX_DEFAULT_THEME || 'default';
theme = sanitize(KNOX_DEFAULT_THEME) || 'default';
} else {
// Normal mode: URL parameter > localStorage > deployment default
theme = urlParams.get('theme');
var urlParams = new URLSearchParams(window.location.search);
var requested = sanitize(urlParams.get('theme'));

// If theme parameter exists, save to localStorage for persistence
if (theme) {
if (requested) {
// Only a validated theme is persisted for future visits
try {
localStorage.setItem('knox-auth-theme', theme);
localStorage.setItem('knox-auth-theme', requested);
themeIsPersisted = true;
} catch (e) {
Comment thread
lmccay marked this conversation as resolved.
// LocalStorage may be disabled, continue without saving
}
theme = requested;
} else {
// Try to load from localStorage, fallback to deployment default
// Try to load from localStorage, fallback to deployment default.
// Stored values are re-validated so an entry saved before this
// validation existed cannot take effect, and is discarded.
try {
theme = localStorage.getItem('knox-auth-theme') || KNOX_DEFAULT_THEME || 'default';
var saved = localStorage.getItem('knox-auth-theme');
theme = sanitize(saved);
if (saved && !theme) {
localStorage.removeItem('knox-auth-theme');
}
themeIsPersisted = theme !== null;
} catch (e) {
// LocalStorage may be disabled, use deployment default
theme = KNOX_DEFAULT_THEME || 'default';
theme = null;
}
theme = theme || sanitize(KNOX_DEFAULT_THEME) || 'default';
}
}

// Load theme CSS if specified and not default
// Use document.write to load synchronously and prevent FOUC
if (theme && theme !== 'default') {
document.write('<link rel="stylesheet" type="text/css" href="styles/themes/' + theme + '/theme.css" id="knox-theme">');
// Load theme CSS if specified and not default.
// Built with DOM APIs rather than document.write so the theme name can
// never be interpreted as markup; appending to head during head parsing
// is still render-blocking, which is what prevents FOUC.
if (theme !== 'default') {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.id = 'knox-theme';
// The server is the authority on which themes exist. If the theme held
// in localStorage fails to load, it is not installed here, so forget it
// straight away rather than replaying it on the next visit. A failure of
// an admin-configured theme is left alone - that is a deployment problem
// to fix, not the user's preference to discard.
if (themeIsPersisted) {
link.onerror = function() {
try {
localStorage.removeItem('knox-auth-theme');
} catch (e) {
// LocalStorage may be disabled, nothing to clean up
}
};
}
link.href = 'styles/themes/' + theme + '/theme.css';
document.head.appendChild(link);
}
})();
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,30 @@ Organizations with branding requirements or compliance needs should use `KNOX_TH

### Theme Name Validation

The theme loader only loads files from `styles/themes/THEME_NAME/theme.css`. Path traversal attacks (e.g., `?theme=../../etc/passwd`) are prevented by the URL structure.
The `?theme=` parameter and the saved localStorage preference are attacker-influenceable,
so the theme loader validates every candidate against `^[a-zA-Z0-9_-]{1,64}$` before it is
stored or used. This rejects quotes, angle brackets, dots and path separators, which
blocks both markup injection and path traversal (e.g. `?theme=../../etc/passwd`).
Validation is applied to values read back from localStorage as well as to the URL
parameter, so a value saved by an earlier visit cannot bypass it, and the stylesheet
element is built with DOM APIs rather than string concatenation.

Because the only URL the loader can produce is `styles/themes/THEME_NAME/theme.css`, the
themes actually installed on the server are the effective allowlist. A name that does not
match an installed theme fails to load, the base Knox styles remain in effect, and the
saved preference is discarded.

### Content Security Policy

If you have strict CSP, ensure it allows:
- Loading CSS from same origin
- Loading fonts from Google Fonts (if using modern theme)

A policy can be applied to the knoxauth route with the WebAppSec provider's
`SecurityHeaderFilter`, which emits arbitrary response headers from its init
parameters. Note that `login.html` currently uses inline scripts and inline event
handlers, so a policy for this page needs `'unsafe-inline'` for `script-src`.

Example CSP:
```
Content-Security-Policy: style-src 'self' https://fonts.googleapis.com;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,9 +441,18 @@ Modern CSS features used:

## Security Considerations

1. **XSS Protection**: Theme names are not executed as code, only used to construct file paths
2. **Path Traversal**: Theme loader only loads files from `styles/themes/` directory
3. **Content Security Policy**: Ensure CSP allows loading external fonts if using Google Fonts
1. **Theme Name Validation**: Theme names arrive from untrusted sources (the `?theme=`
URL parameter and the saved localStorage preference), so each candidate must match
`^[a-zA-Z0-9_-]{1,64}$` before it is stored or used. Validation is applied on the
localStorage read path as well as the URL, and a value that fails is discarded.
2. **XSS Protection**: The stylesheet element is created with DOM APIs
(`document.createElement`) rather than by concatenating markup, so a theme name can
never be parsed as HTML.
3. **Path Traversal**: The validation pattern rejects dots and path separators, so the
only URL the loader can produce is `styles/themes/THEME_NAME/theme.css`. A name that
does not correspond to an installed theme simply fails to load and the base styles
remain in effect - the themes present on the server are the effective allowlist.
4. **Content Security Policy**: Ensure CSP allows loading external fonts if using Google Fonts

## Troubleshooting

Expand Down
Loading