This is an automated email from the ASF dual-hosted git repository.

lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new e51163532 KNOX-3409: Validate knoxauth theme name to Harden Branding 
Support (Reported by Quinn Nguyen) (#1341)
e51163532 is described below

commit e51163532f705b1370540f9ce76f68008ff6bd5b
Author: lmccay <[email protected]>
AuthorDate: Tue Aug 11 00:14:31 2026 -0400

    KNOX-3409: Validate knoxauth theme name to Harden Branding Support 
(Reported by Quinn Nguyen) (#1341)
    
    * KNOX-3409: Validate knoxauth theme name to prevent DOM-based XSS
    
    The knoxauth login page read the "theme" query parameter and wrote it into
    a <link> tag via document.write() with no validation, allowing arbitrary
    markup injection on the page that collects user credentials. The value was
    also persisted to localStorage and replayed on later visits, so a single
    malicious link kept executing on subsequent visits from a clean URL.
    
    Theme names are now validated against ^[a-zA-Z0-9_-]{1,64}$ on every path
    (URL parameter, localStorage, and the configured default), which rejects
    quotes, angle brackets, dots and path separators. The stylesheet element is
    built with DOM APIs instead of string concatenation, so a theme name can
    never be parsed as markup. A stored value that fails validation, or that
    does not resolve to an installed theme, is discarded rather than replayed.
    
    Because the only URL the loader can produce is 
styles/themes/<name>/theme.css,
    the themes actually installed on the server remain the effective allowlist 
and
    no new configuration is required. The README and deployment guide are 
updated
    to describe the validation, replacing two security claims that were 
inaccurate
    as written.
    
    Reported by Quinn Nguyen. Not present in any released version - the theming
    feature (KNOX-3283) has only ever been on master.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * KNOX-3409: Discard an unresolvable theme preference on the same visit
    
    Addresses review feedback. The cleanup flag tracked whether the theme had
    been read from localStorage rather than whether it is currently held there.
    A theme supplied via ?theme= is persisted immediately, so it is equally
    eligible for cleanup, but the flag stayed false and no onerror handler was
    attached. A name that passed validation without resolving to an installed
    theme was therefore saved and replayed once before being cleared.
    
    The flag is renamed to themeIsPersisted and set after a successful
    setItem, so it reflects the invariant the cleanup actually depends on.
    Setting it inside the try means a failed write - localStorage disabled -
    correctly leaves nothing to clean up. Behaviour for an admin-configured
    theme is unchanged: a deployment error does not discard a user preference.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../resources/applications/knoxauth/app/login.html | 72 ++++++++++++++++++----
 .../knoxauth/app/styles/themes/DEPLOYMENT.md       | 18 +++++-
 .../knoxauth/app/styles/themes/README.md           | 15 ++++-
 3 files changed, 88 insertions(+), 17 deletions(-)

diff --git 
a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html 
b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
index 8c69ec9c2..6a30f6106 100644
--- 
a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
+++ 
b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
@@ -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) {
                            // 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>
diff --git 
a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/DEPLOYMENT.md
 
b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/DEPLOYMENT.md
index 220c3177a..e746fdd33 100644
--- 
a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/DEPLOYMENT.md
+++ 
b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/DEPLOYMENT.md
@@ -332,7 +332,18 @@ 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
 
@@ -340,6 +351,11 @@ 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;
diff --git 
a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/README.md
 
b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/README.md
index e15aa7721..4bbfd5872 100644
--- 
a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/README.md
+++ 
b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/themes/README.md
@@ -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
 

Reply via email to