Skip to content

Automated Test: feature-html-sanitizer-implementation #316

Closed
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 @@ -185,7 +185,7 @@ webauthn-help-text=Use your Passkey to sign in.
webauthn-passwordless-display-name=Passkey
webauthn-passwordless-help-text=Use your Passkey for passwordless sign in.
passwordless=Passwordless
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} {2,choice,0#values|1#value|1<values}.
recovery-authn-code=My recovery authentication codes
recovery-authn-codes-display-name=Recovery authentication codes
recovery-authn-codes-help-text=These codes can be used to regain your access in case your other 2FA means are not available.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,7 @@ onDragCancel=已取消拖动。列表未更改。
removeUser=移除用户
ownerManagedAccess=启用用户管理访问
userModelAttributeNameHelp=从 LDAP 导入用户时要添加的模型属性的名称
templateHelp=用于格式化要导入的用户名的模板。替换包含在 ${} 中。例如:'${ALIAS}.${CLAIM.sub}'。ALIAS 是供应商别名。CLAIM.<NAME > 引用 ID 或访问令牌声明。可以通过将 |uppercase 或 |lowercase 附加到替换值来将替换转换为大写或小写,例如“${CLAIM.sub | lowercase}”。
templateHelp=用于格式化要导入的用户名的模板。替换包含在 ${} 中。例如:'${ALIAS}.${CLAIM.sub}'。ALIAS 是供应商别名。CLAIM.<NAME> 引用 ID 或访问令牌声明。可以通过将 |uppercase 或 |lowercase 附加到替换值来将替换转换为大写或小写,例如“${CLAIM.sub | lowercase}”。
permissions=权限
emptyExecutionInstructions=您可以通过添加子流程或执行器来开始定义此流程
offlineSessionSettings=离线会话设置
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3133,7 +3133,7 @@ bruteForceMode.PermanentLockout=Lockout permanently
bruteForceMode.TemporaryLockout=Lockout temporarily
bruteForceMode.PermanentAfterTemporaryLockout=Lockout permanently after temporary lockout
bruteForceMode=Brute Force Mode
error-invalid-multivalued-size=Attribute {{0}} must have at least {{1}} and at most {{2}} value(s).
error-invalid-multivalued-size=Attribute {0} must have at least {1} and at most {2} {2,choice,0#values|1#value|1<values}.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check the file structure and understand where and how messages are used
find js/apps/admin-ui -type f -name "*.properties" | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1542


🏁 Script executed:

#!/bin/bash
# Search for message formatting usage in the admin-ui
rg -n --type=ts --type=tsx --type=js --type=jsx 'formatMessage|MessageFormat|messages_en|IntlMessageFormat' js/apps/admin-ui --max-count=50

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Look for build configuration files that might process .properties
find js/apps/admin-ui -name "pom.xml" -o -name "package.json" -o -name "webpack.config.*" -o -name "tsconfig.json" | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 211


🏁 Script executed:

#!/bin/bash
# Search for message formatting in admin-ui with correct file types
rg -n --type=ts --type=js 'formatMessage|MessageFormat|messages_en|IntlMessageFormat' js/apps/admin-ui --max-count=100

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Check the package.json for formatting libraries
cat js/apps/admin-ui/package.json | grep -A 10 -B 10 '"dependencies"' | head -50

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1464


🏁 Script executed:

#!/bin/bash
# Check the pom.xml to see how properties are processed
cat js/apps/admin-ui/pom.xml | head -100

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 3071


🏁 Script executed:

#!/bin/bash
# Look for where messages_en.properties is actually loaded/used
rg -n 'messages_en|messages\.properties|messages_' js/apps/admin-ui/src --type=ts --type=js

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Check full package.json dependencies and devDependencies
cat js/apps/admin-ui/package.json

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 3970


🏁 Script executed:

#!/bin/bash
# Search for intl, i18n, or messageformat related packages more broadly
rg -n 'intl|i18n|messageformat|formatjs' js/apps/admin-ui/package.json js/apps/admin-ui/pnpm-lock.yaml 2>/dev/null | head -50

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 284


🏁 Script executed:

#!/bin/bash
# Check the vite config to understand how resources are handled
cat js/apps/admin-ui/vite.config.ts 2>/dev/null || cat js/apps/admin-ui/vite.config.js 2>/dev/null

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1538


🏁 Script executed:

#!/bin/bash
# Search for i18next initialization and configuration
rg -n 'i18n\s*=' js/apps/admin-ui/src --type=ts --type=js -A 10 | head -100

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 770


🏁 Script executed:

#!/antml/bash
# Look for i18next configuration files
find js/apps/admin-ui -name "*i18n*" -o -name "*translation*" -o -name "*locale*" | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 278


🏁 Script executed:

#!/bin/bash
# Search for any reference to intl-messageformat in the entire repo
rg -r 'intl-messageformat|ICUFormat|Parser.*ICU' . 2>/dev/null | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 25013


🏁 Script executed:

#!/bin/bash
# Check main.tsx or index files to see i18next setup
cat js/apps/admin-ui/src/main.tsx

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 743


🏁 Script executed:

#!/bin/bash
# Read the complete i18n initialization file
cat js/apps/admin-ui/src/i18n/i18n.ts

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1043


🏁 Script executed:

#!/bin/bash
# Read the translation formatter to see if custom ICU handling exists
cat js/apps/admin-ui/src/utils/translationFormatter.ts

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 419


🏁 Script executed:

#!/bin/bash
# Check what the label function from keycloak-ui-shared does
find . -path ./node_modules -prune -o -name "*.ts" -type f -exec grep -l "export.*label" {} \; | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Look for keycloak-ui-shared source to understand the label function
find . -path "*keycloak-ui-shared*" -name "*.ts" -o -name "*.tsx" | head -30

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 1575


🏁 Script executed:

#!/bin/bash
# Check if there's any ICU or intl-messageformat usage in the shared library
rg -n 'ICU|intl-messageformat|choice' libs/keycloak-ui-shared 2>/dev/null | head -50

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Find the label function in keycloak-ui-shared
rg -n "export.*label" js/libs/ui-shared/src --type=ts -A 5 | head -100

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Look at the main index file of keycloak-ui-shared
cat js/libs/ui-shared/src/index.ts

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Read the label function implementation completely
cat js/libs/ui-shared/src/user-profile/utils.ts

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 4134


🏁 Script executed:

#!/bin/bash
# Check if there's a main index or export file in ui-shared
ls -la js/libs/ui-shared/src/ | head -20

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 972


🌐 Web query:

i18next ICU MessageFormat choice syntax support

💡 Result:

i18next can use ICU MessageFormat only via a plugin such as i18next-icu, which swaps i18next’s default interpolation/plural system for FormatJS intl-messageformat parsing/formatting. [1]

What ICU “choice” syntax support means in this setup

  • i18next-icu supports the ICU MessageFormat arguments that intl-messageformat supports: plural, select, and selectordinal (plus number/date/time formatting). [2]
  • It does not provide Java/ICU4J-style choice formatting. In ICU/MessageFormat history, “choice” exists as a format type alongside plural/select in ICU implementations, but FormatJS’s intl-messageformat feature set is centered on plural/select/selectordinal rather than “choice”. [2][3]

What to use instead of choice

  • For numeric branching, use plural (with exact matches like =0, =1, etc.). [2]
  • For non-numeric branching (e.g., gender/status), use select (must include other). [2][3]

Example

"{count, plural, =0 {No items} one {One item} other {# items}}"
"{gender, select, male {He} female {She} other {They}}"

Sources
[1] i18next-icu README (uses FormatJS intl-messageformat)
[2] FormatJS intl-messageformat docs (supported: plural/select/selectordinal)
[3] ICU docs noting “choice” as a distinct format type in ICU MessageFormat implementations


Replace ICU choice syntax with i18next-compatible plural syntax or add i18next-icu support.

The admin-ui formatter uses i18next without the i18next-icu plugin, so the ICU choice pattern {2,choice,0#values|1#value|1<values} will render literally in the UI instead of being formatted. Either install and configure i18next-icu to enable ICU MessageFormat support, or rewrite the message using i18next's native plural syntax: {2, plural, =0 {values} one {value} other {values}}.

🤖 Prompt for AI Agents
In
`@js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties`
at line 3136, The property for key error-invalid-multivalued-size uses ICU
choice syntax which i18next in admin-ui doesn't support; either enable and
configure i18next-icu in the app or change the value for
error-invalid-multivalued-size to use i18next plural syntax by replacing the ICU
choice segment with an i18next plural expression for the third placeholder (use
forms for =0, one, other) so the correct singular/plural form is rendered at
runtime.

multivalued=Multivalued
multivaluedHelp=If this attribute supports multiple values. This setting is an indicator and does not enable any validation.
to the attribute. For that, make sure to use any of the built-in validators to properly validate the size and the values.
Expand Down
11 changes: 11 additions & 0 deletions misc/theme-verifier/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20240325.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.13.0</version>
<scope>compile</scope>
</dependency>
Comment on lines +75 to +85

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

CVE owasp-java-html-sanitizer 20240325.1 security vulnerability

💡 Result:

Vulnerability: CVE-2025-66021 (GitHub advisory GHSA-g9gq-3pfx-2gw2) affects OWASP Java HTML Sanitizer (Maven com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer) version 20240325.1. [1][2]

What it is: A cross-site scripting (XSS) issue that can occur only in a specific configuration: when HtmlPolicyBuilder allows both <noscript> and <style> and uses .allowTextIn("style"). In that edge case, a crafted payload can cause script to survive sanitization and execute in the browser. [1][2]

Severity: CVSS v4 base score 8.6 (High) as reported by the CNA (GitHub). [2]

Fix / patched version: Upgrade to 20260101.1, which GitHub lists as the patched release for this advisory. [1]

Mitigation (if you can’t upgrade immediately):

  • Avoid the vulnerable policy combination: don’t allow noscript, and/or don’t use allowTextIn("style") (or don’t allow style). [1][2]

Sources: GitHub Advisory Database [1], NVD [2].


🌐 Web query:

CVE commons-text 1.13.0 security vulnerability

💡 Result:

For Apache Commons Text 1.13.0, the well-known Commons Text RCE issue (“Text4Shell”, CVE-2022-42889) does not apply, because the affected range is Commons Text 1.5–1.9 (i.e., versions prior to 1.10.0) and the mitigation was released in 1.10.0 (safer defaults for interpolation lookups) [1][2][3].

What to do

  • If you are on 1.13.0, you are already past the affected versions for CVE-2022-42889 [1][2].
  • Still, follow Apache’s guidance: do not pass untrusted input into StringSubstitutor without validation/sanitization, because exploitation depends on how the API is used [1][3].

Sources
[1] Apache Commons Text Security Reports (CVE-2022-42889 applies to versions prior to 1.10.0)
[2] NVD: CVE-2022-42889 (affected versions 1.5 through 1.9; upgrade to 1.10.0+)
[3] Apache Security blog: CVE-2022-42889 details and mitigation in Commons Text 1.10.0


🏁 Script executed:

#!/bin/bash
# Check Maven Central for these dependencies and their latest versions

echo "=== Checking owasp-java-html-sanitizer versions ==="
curl -s "https://repo1.maven.org/maven2/com/googlecode/owasp-java-html-sanitizer/owasp-java-html-sanitizer/" | grep -oP '(20[0-9]{6}\.[0-9]+|jar|pom)' | sort -u | head -20

echo ""
echo "=== Checking commons-text versions ==="
curl -s "https://repo1.maven.org/maven2/org/apache/commons/commons-text/" | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -10

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 468


Upgrade OWASP HTML Sanitizer to patch critical XSS vulnerability.

The dependency owasp-java-html-sanitizer:20240325.1 contains CVE-2025-66021 (GHSA-g9gq-3pfx-2gw2), a high-severity XSS vulnerability (CVSS 8.6). The vulnerability occurs in a specific policy configuration when HtmlPolicyBuilder allows both <noscript> and <style> with .allowTextIn("style"), allowing crafted payloads to bypass sanitization. Upgrade to version 20260101.1 or later.

The commons-text:1.13.0 dependency is secure and appropriate for the feature.

🤖 Prompt for AI Agents
In `@misc/theme-verifier/pom.xml` around lines 75 - 85, Update the OWASP HTML
Sanitizer dependency to a patched release: change the owasp-java-html-sanitizer
dependency (artifactId owasp-java-html-sanitizer, groupId
com.googlecode.owasp-java-html-sanitizer) version from 20240325.1 to 20260101.1
(or later) in the pom.xml; after updating, rebuild/resolve dependencies to
ensure the new version is used and run tests that exercise HtmlPolicyBuilder
usages to confirm no regressions.

</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@
package org.keycloak.themeverifier;

import org.apache.maven.plugin.MojoExecutionException;
import org.owasp.html.PolicyFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.PropertyResourceBundle;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class VerifyMessageProperties {

Expand All @@ -41,12 +48,129 @@ public List<String> verify() throws MojoExecutionException {
try {
String contents = Files.readString(file.toPath());
verifyNoDuplicateKeys(contents);
verifySafeHtml();
} catch (IOException e) {
throw new MojoExecutionException("Can not read file " + file, e);
}
return messages;
}

PolicyFactory POLICY_SOME_HTML = new org.owasp.html.HtmlPolicyBuilder()
.allowElements(
"br", "p", "strong", "b"
).toFactory();

PolicyFactory POLICY_NO_HTML = new org.owasp.html.HtmlPolicyBuilder().toFactory();

private void verifySafeHtml() {
PropertyResourceBundle bundle;
try (FileInputStream fis = new FileInputStream(file)) {
bundle = new PropertyResourceBundle(fis);
} catch (IOException e) {
throw new RuntimeException("unable to read file " + file, e);
}

PropertyResourceBundle bundleEnglish;
String englishFile = file.getAbsolutePath().replaceAll("resources-community", "resources")
.replaceAll("_[a-zA-Z-_]*\\.properties", "_en.properties");
try (FileInputStream fis = new FileInputStream(englishFile)) {
bundleEnglish = new PropertyResourceBundle(fis);
} catch (IOException e) {
throw new RuntimeException("unable to read file " + englishFile, e);
}

bundle.getKeys().asIterator().forEachRemaining(key -> {
String value = bundle.getString(key);
value = normalizeValue(key, value);
String englishValue = getEnglishValue(key, bundleEnglish);
englishValue = normalizeValue(key, englishValue);

value = santizeAnchors(key, value, englishValue);

// Only if the English source string contains HTML we also allow HTML in the translation
PolicyFactory policy = containsHtml(englishValue) ? POLICY_SOME_HTML : POLICY_NO_HTML;
String sanitized = policy.sanitize(value);

// Sanitizer will escape HTML entities for quotes and also for numberic tags like '<1>'
sanitized = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(sanitized);
// Sanitizer will add them when there are double curly braces
sanitized = sanitized.replace("<!-- -->", "");

if (!Objects.equals(sanitized, value)) {

// Strip identical characters from the beginning and the end to show where the difference is
int start = 0;
while (start < sanitized.length() && start < value.length() && value.charAt(start) == sanitized.charAt(start)) {
start++;
}
int end = 0;
while (end < sanitized.length() && end < value.length() && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
end++;
}

messages.add("Illegal HTML in key " + key + " for file " + file + ": '" + value.substring(start, value.length() - end) + "' vs. '" + sanitized.substring(start, sanitized.length() - end) + "'");
Comment on lines +99 to +111

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential IllegalArgumentException in substring when strings overlap.

If start + end >= value.length() (which can happen when the strings are very similar with only a small difference), the substring call value.substring(start, value.length() - end) will have start > endIndex, causing an exception.

🐛 Proposed fix to handle overlap
             if (!Objects.equals(sanitized, value)) {

                 // Strip identical characters from the beginning and the end to show where the difference is
                 int start = 0;
                 while (start < sanitized.length() && start < value.length() && value.charAt(start) == sanitized.charAt(start)) {
                     start++;
                 }
                 int end = 0;
-                while (end < sanitized.length() && end < value.length() && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
+                while (end < sanitized.length() - start && end < value.length() - start && value.charAt(value.length() - end - 1) == sanitized.charAt(sanitized.length() - end - 1)) {
                     end++;
                 }

                 messages.add("Illegal HTML in key " + key + " for file " + file + ": '" + value.substring(start, value.length() - end) + "' vs. '" + sanitized.substring(start, sanitized.length() - end) + "'");
             }
🤖 Prompt for AI Agents
In
`@misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java`
around lines 99 - 111, The substring calls in VerifyMessageProperties (inside
the block checking Objects.equals(sanitized, value)) can throw
IllegalArgumentException when start + end >= value.length(); adjust the indices
before calling substring on value and sanitized: compute endIndexValue =
Math.max(start, value.length() - end) and endIndexSanitized = Math.max(start,
sanitized.length() - end) (or detect start >= value.length() - end and use an
empty string for that side), then use those safe indices in the messages.add
call so substring(start, endIndexValue) and substring(start, endIndexSanitized)
never receive start > endIndex; keep the existing start/end computation and only
guard the final substring boundaries to avoid exceptions.

}

});
}

private String normalizeValue(String key, String value) {
if (key.equals("templateHelp")) {
// Allow "CLAIM.<NAME>" here
value = value.replaceAll("CLAIM\\.<[A-Z]*>", "");
} else if (key.equals("optimizeLookupHelp")) {
// Allow "<Extensions>" here
value = value.replaceAll("<Extensions>", "");
} else if (key.startsWith("linkExpirationFormatter.timePeriodUnit") || key.equals("error-invalid-multivalued-size")) {
// The problem is the "<" that appears in the choice
value = value.replaceAll("\\{[0-9]+,choice,[^}]*}", "...");
}

// Unescape HTML entities, as we later also unescape HTML entities in the sanitized value
value = org.apache.commons.text.StringEscapeUtils.unescapeHtml4(value);

if (file.getAbsolutePath().contains("email")) {
// TODO: move the RTL information for emails
value = value.replaceAll(Pattern.quote(" style=\"direction: rtl;\""), "");
}
return value;
}

Pattern HTML_TAGS = Pattern.compile("<[a-z]+[^>]*>");

private boolean containsHtml(String englishValue) {
return HTML_TAGS.matcher(englishValue).find();
}

private static final Pattern ANCHOR_PATTERN = Pattern.compile("</?a[^>]*>");

/**
* Allow only those anchor tags from the source key to also appear in the target key.
*/
private String santizeAnchors(String key, String value, String englishValue) {
Matcher matcher = ANCHOR_PATTERN.matcher(value);
Matcher englishMatcher = ANCHOR_PATTERN.matcher(englishValue);
while (matcher.find()) {
if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) {
value = value.replaceFirst(Pattern.quote(englishMatcher.group()), "");
} else {
messages.add("Didn't find anchor tag " + matcher.group() + " in original string");
break;
}
}
return value;
}

private static String getEnglishValue(String key, PropertyResourceBundle bundleEnglish) {
String englishValue;
try {
englishValue = bundleEnglish.getString(key);
} catch (MissingResourceException ex) {
englishValue = "";
}
return englishValue;
}

private void verifyNoDuplicateKeys(String contents) throws IOException {
BufferedReader bufferedReader = new BufferedReader(new StringReader(contents));
String line;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,26 @@ class VerifyMessagePropertiesTest {

@Test
void verifyDuplicateKeysDetected() throws MojoExecutionException {
List<String> verify = getFile("duplicate_keys.properties").verify();
MatcherAssert.assertThat(verify, Matchers.contains(Matchers.containsString("Duplicate keys in file")));
List<String> verify = getFile("duplicateKeys_en.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Duplicate keys in file")));
}

@Test
void verifyIllegalHtmlTagDetected() throws MojoExecutionException {
List<String> verify = getFile("illegalHtmlTag_en.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Illegal HTML")));
}

@Test
void verifyNoHtmlAllowed() throws MojoExecutionException {
List<String> verify = getFile("noHtml_de.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Illegal HTML")));
}

@Test
void verifyNoChangedAnchors() throws MojoExecutionException {
List<String> verify = getFile("changedAnchor_de.properties").verify();
MatcherAssert.assertThat(verify, Matchers.hasItem(Matchers.containsString("Didn't find anchor tag")));
}

private static VerifyMessageProperties getFile(String fixture) {
Expand Down
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/changedAnchor_de.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <a href="http://malicious.com">link</a>
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/changedAnchor_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <a href="http://example.com">link</a>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <div>tag</div
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/noHtml_de.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=Some <b>HTML</b>
17 changes: 17 additions & 0 deletions misc/theme-verifier/src/test/resources/noHtml_en.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#
# Copyright 2025 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
key=No HTML
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ revoke=Kumoa oikeudet

configureAuthenticators=Konfiguroitu kaksivaiheinen kirjautuminen
mobile=Mobiili
totpStep1=Asenna <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> tai Google Authenticator ohjelma laiteellesi. Kummatkin sovellukset ovat saatavilla <a href="https://play.google.com">Google Play</a> ja Apple App Store kaupoissa.
totpStep1=Asenna jokin seuraavista sovelluksista matkapuhelimeesi:
totpStep2=Avaa sovellus ja skannaa QR-koodi tai kirjoita avain.
totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix Finnish typo in instruction text.

“allaolevaan” should be “alla olevaan”.

🔧 Proposed fix
-totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna.
+totpStep3=Täytä saamasi kertaluontoinen koodisi alla olevaan kenttään ja paina Tallenna.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
totpStep3=Täytä saamasi kertaluontoinen koodisi allaolevaan kenttään ja paina Tallenna.
totpStep3=Täytä saamasi kertaluontoinen koodski alla olevaan kenttään ja paina Tallenna.
🤖 Prompt for AI Agents
In
`@themes/src/main/resources-community/theme/base/account/messages/messages_fi.properties`
at line 143, The property totpStep3 contains a Finnish typo: change the value
text by replacing "allaolevaan" with the correct two-word form "alla olevaan" so
the string for totpStep3 reads with "alla olevaan" instead of "allaolevaan".

totpStep3DeviceName=Anna laitteelle nimi, jotta voit hallinnoida OTP-laitteitasi.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ revoke=Atšaukti įgaliojimą

configureAuthenticators=Sukonfigūruotas autentifikatorius
mobile=Mobilus
totpStep1=Įdiekite <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> arba Google Authenticator savo įrenginyje. Programėlės prieinamos <a href="https://play.google.com">Google Play</a> ir Apple App Store.
totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check its structure
find . -name "messages_lt.properties" -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 642


🏁 Script executed:

# Check the content around line 101
if [ -f "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties" ]; then
  echo "=== Lines 99-105 of messages_lt.properties ==="
  sed -n '99,105p' "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties"
  echo ""
  echo "=== File statistics ==="
  wc -l "themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties"
fi

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 642


🏁 Script executed:

# Search for the Italian text pattern mentioned in the review across all locale files
echo "=== Searching for Italian text in non-Italian locale files ==="
rg -n "Installa una delle|sul tuo cellulare|seguenti applicazioni" \
  --glob '**/messages_*.properties' \
  --glob '!**/messages_it.properties' \
  -A 0 -B 0

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 473


🏁 Script executed:

# Also check if there's an Italian locale file to compare
echo "=== Checking for Italian locale file ==="
find . -name "messages_it.properties" -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 582


🏁 Script executed:

# If Italian locale file exists, check line 101 there for comparison
if [ -f "themes/src/main/resources-community/theme/base/account/messages/messages_it.properties" ]; then
  echo "=== Lines 99-105 of messages_it.properties (for comparison) ==="
  sed -n '99,105p' "themes/src/main/resources-community/theme/base/account/messages/messages_it.properties"
fi

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 297


Critical localization error: Italian text in Lithuanian locale files.

The value for totpStep1 contains Italian text ("Installa una delle seguenti applicazioni sul tuo cellulare:") instead of Lithuanian. This breaks the user experience for Lithuanian users, who will see mixed languages during TOTP setup (Italian for step 1, Lithuanian for steps 2-3).

The same Italian text also appears in themes/src/main/resources-community/theme/base/login/messages/messages_lt.properties at line 71 under loginTotpStep1.

🌍 Proposed fix with Lithuanian translation
-totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:
+totpStep1=Įdiekite vieną iš šių programėlių į savo mobilųjį telefoną:

Apply the same fix to login/messages/messages_lt.properties line 71 for loginTotpStep1.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:
totpStep1=Įdiekite vieną iš šių programėlių į savo mobilųjį telefoną:
🤖 Prompt for AI Agents
In
`@themes/src/main/resources-community/theme/base/account/messages/messages_lt.properties`
at line 101, Replace the Italian string for totpStep1 and loginTotpStep1 with a
proper Lithuanian translation so both keys show Lithuanian during TOTP setup;
locate the entries named totpStep1 and loginTotpStep1 and change "Installa una
delle seguenti applicazioni sul tuo cellulare:" to the Lithuanian equivalent
(e.g., "Įdiekite vieną iš šių programėlių savo telefone:") in both
messages_lt.properties files.

totpStep2=Atidarykite programėlę ir nuskenuokite barkodą arba įveskite kodą.
totpStep3=Įveskite programėlėje sugeneruotą vieną kartą galiojantį kodą ir paspauskite Saugoti norėdami prisijungti.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ revoke=Zrušiť oprávnenie

configureAuthenticators=Nakonfigurované autentifikátory
mobile=Mobilný
totpStep1=Nainštalujte vo svojom zariadení <a href="https://freeotp.github.io/" target="_blank"> FreeOTP </a> alebo Google Authenticator. Obidve aplikácie sú k dispozícii v <a href="https://play.google.com"> Google Play </a> a Apple App Store.
totpStep1=Nainštalujte si do mobilu jednu z nasledujúcich aplikácií:
totpStep2=Otvorte aplikáciu a naskenujte čiarový kód alebo zadajte kľúč.
totpStep3=Zadajte jednorazový kód poskytnutý aplikáciou a kliknutím na tlačidlo Uložiť dokončíte nastavenie.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ revoke=Upphäv rättighet

configureAuthenticators=Konfigurerade autentiserare
mobile=Mobil
totpStep1=Installera <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> eller Google Authenticator på din enhet. Båda applikationerna finns tillgängliga på <a href="https://play.google.com">Google Play</a> och Apple App Store.
totpStep1=Installera en av följande applikationer på din mobil:
totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.
Comment on lines +96 to 98

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor Swedish grammar correction needed in totpStep2.

The phrase "skriv i nyckeln" in line 97 is grammatically awkward in Swedish. The preposition "i" suggests writing "inside" something, but for entering or typing a key, Swedish uses "skriv in" (write/enter) or "ange" (enter/specify).

📝 Suggested correction
 totpStep1=Installera en av följande applikationer på din mobil:
-totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
+totpStep2=Öppna applikationen och skanna streckkoden eller skriv in nyckeln.
 totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.

Alternatively, you could use "ange nyckeln" (enter the key) for a more formal tone:

-totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
+totpStep2=Öppna applikationen och skanna streckkoden eller ange nyckeln.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
totpStep1=Installera en av följande applikationer på din mobil:
totpStep2=Öppna applikationen och skanna streckkoden eller skriv i nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.
totpStep1=Installera en av följande applikationer på din mobil:
totpStep2=Öppna applikationen och skanna streckkoden eller skriv in nyckeln.
totpStep3=Fyll i engångskoden som tillhandahålls av applikationen och klicka på Spara för att avsluta inställningarna.
🤖 Prompt for AI Agents
In
`@themes/src/main/resources-community/theme/base/account/messages/messages_sv.properties`
around lines 96 - 98, The Swedish phrase for entering a key is awkward; update
the value for the properties key totpStep2 to use the correct verb form (e.g.,
replace "skriv i nyckeln" with "skriv in nyckeln" or "ange nyckeln") so the line
reads naturally when displayed to users.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ revoke=收回授权

configureAuthenticators=配置的认证者
mobile=手机
totpStep1=在你的设备上安装 <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> 或者 Google Authenticator.两个应用可以从 <a href="https://play.google.com">Google Play</a> 和 Apple App Store下载。
totpStep1=在您的手機上安裝以下應用程式之一:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Simplified Chinese wording for zh_CN.

The updated text uses Traditional characters (“手機”, “應用程式”) while this file is zh_CN and surrounding strings are Simplified. Consider using “手机 / 应用程序” to keep locale consistency.

✏️ Suggested fix
-totpStep1=在您的手機上安裝以下應用程式之一:
+totpStep1=在您的手机上安装以下应用程序之一:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
totpStep1=在您的手機上安裝以下應用程式之一
totpStep1=在您的手机上安装以下应用程序之一
🤖 Prompt for AI Agents
In
`@themes/src/main/resources-community/theme/base/account/messages/messages_zh_CN.properties`
at line 112, The string for key totpStep1 uses Traditional Chinese characters
("手機", "應用程式") but this file is zh_CN; replace those words with Simplified
Chinese equivalents ("手机", "应用程序") so the totpStep1 value matches the locale and
surrounding entries.

totpStep2=打开应用扫描二维码输入验证码
totpStep3=输入应用提供的一次性验证码单击保存

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ emailTestBody=Dette er en test besked
emailTestBodyHtml=<p>Dette er en test besked</p>
identityProviderLinkSubject=Link {0}
identityProviderLinkBody=Nogen vil forbinde din "{1}" konto med "{0}" kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti\n\n{3}\n\nDette link vil udløbe efter {5}.\n\nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor typo: "nHvis" should be "Hvis"

The HTML structure improvements look good with proper closing tags and punctuation. However, there's a typo in the last paragraph where "nHvis" should be "Hvis" (without the leading "n").

📝 Proposed fix for the typo
-identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
+identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>Hvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>nHvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
identityProviderLinkBodyHtml=<p>Nogen vil forbinde din <b>{1}</b> konto med <b>{0}</b> kontoen som er tilknyttet brugeren {2}. Hvis dette var dig, bedes du klikke på forbindet herunder for at forbinde de to konti</p><p><a href="{3}">Bekræft</a></p><p>Dette link vil udløbe efter {5}.</p><p>Hvis du ikke vil forbinde disse konti, kan du bare ignore denne besked. Hvis du vælger at forbinde de to konti, kan du logge ind som {1} via {0}.</p>
🤖 Prompt for AI Agents
In
`@themes/src/main/resources-community/theme/base/email/messages/messages_da.properties`
at line 9, Fix the typo in the message value for the key
identityProviderLinkBodyHtml: remove the stray leading "n" before "Hvis" in the
last paragraph so it reads "Hvis du ikke vil forbinde disse konti..." preserving
the rest of the HTML and placeholders unchanged.

passwordResetSubject=Gendan adgangskode
passwordResetBody=Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.\n\n{0}\n\nDette link og kode vil udløbe efter {3}.\n\nHvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked.
passwordResetBodyHtml=<p>Nogen har forsøgt at nulstille adgangskoden til {2}. Hvis dette var dig, bedes du klikke på linket herunder for at nulstille adgangskoden.</p><p><a href="{0}">Nulstil adgangskode</a></p><p>Dette link og kode vil udløbe efter {3}.</p><p>Hvis du ikke ønsker at nulstille din adgangskode, kan du se bort fra denne besked.</p>
Expand Down
Loading