Skip to content

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

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}.
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

🏁 Script executed:

find . -name "pom.xml" -path "*/misc/theme-verifier/*" -type f

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 116


🏁 Script executed:

cat -n misc/theme-verifier/pom.xml | sed -n '70,90p'

Repository: admin-coderabbit/coderabbit-keycloak-mirror

Length of output: 900


🌐 Web query:

owasp-java-html-sanitizer 20240325.1 maven latest version

💡 Result:

The latest Maven Central release of com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer is 20260102.1 (released Jan 3, 2026). [1][2]

If you’re currently on 20240325.1, that is an older release (Mar 25, 2024). [3]

Maven:

<dependency>
  <groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
  <artifactId>owasp-java-html-sanitizer</artifactId>
  <version>20260102.1</version>
</dependency>

Sources

[1] https://repo1.maven.org/maven2/com/googlecode/owasp-java-html-sanitizer/owasp-java-html-sanitizer/
[2] https://mvnrepository.com/artifact/com.googlecode.owasp-java-html-sanitizer/owasp-java-html-sanitizer/20260102.1
[3] https://repo1.maven.org/maven2/com/googlecode/owasp-java-html-sanitizer/owasp-java-html-sanitizer/20240325.1/


🌐 Web query:

commons-text 1.13.0 CVE vulnerabilities security

💡 Result:

commons-text 1.13.0 — CVE / security status (as of Feb 2026)

  • Direct CVEs in org.apache.commons:commons-text 1.13.0: None listed by the Apache Commons Text project. The project’s published security page only calls out CVE-2022-42889, which affects versions prior to 1.10.0—so 1.13.0 is not in the affected range. (commons.apache.org)
  • Transitive/dependency CVEs impacting a default 1.13.0 build: commons-text 1.13.0 has a compile dependency on org.apache.commons:commons-lang3:3.17.0, which is flagged as vulnerable. (mvnrepository.com)
    • The relevant issue is CVE-2025-48924 (Apache Commons Lang): an uncontrolled recursion / StackOverflowError (DoS) risk in ClassUtils.getClass(...) on very long inputs, affecting commons-lang3 < 3.18.0. (nvd.nist.gov)

Recommended remediation

  • Best: Upgrade to a newer Commons Text release (e.g., commons-text 1.15.0), which depends on commons-lang3 3.20.0 (not in the vulnerable range for CVE-2025-48924). (commons.apache.org)
  • Alternative: Keep commons-text 1.13.0 but override/pin commons-lang3 to 3.18.0+ in your dependency management (Maven/Gradle). (nvd.nist.gov)

Sources

[1] Apache Commons Text — Security Vulnerabilities (commons.apache.org)
[2] MvnRepository — commons-text 1.13.0 dependencies (shows commons-lang3 3.17.0) (mvnrepository.com)
[3] NVD — CVE-2025-48924 (Apache Commons Lang) (nvd.nist.gov)
[4] Apache Commons Text — Project Dependencies (1.15.0 uses commons-lang3 3.20.0) (commons.apache.org)

Citations:


Critical: Outdated and vulnerable dependencies require immediate attention.

The owasp-java-html-sanitizer version 20240325.1 is outdated (current: 20260102.1, released Jan 3, 2026). More critically, commons-text 1.13.0 introduces a transitive dependency on commons-lang3 3.17.0, which is vulnerable to CVE-2025-48924 (uncontrolled recursion / DoS in ClassUtils.getClass(...)).

Fixes required:

  • Upgrade commons-text to 1.15.0 or newer (which depends on commons-lang3 3.20.0), OR override commons-lang3 to 3.18.0+ in dependency management.
  • Upgrade owasp-java-html-sanitizer to 20260102.1 or later.

Additionally, the owasp-java-html-sanitizer dependency lacks an explicit <scope>compile</scope> declaration for consistency with commons-text.

🤖 Prompt for AI Agents
In `@misc/theme-verifier/pom.xml` around lines 75 - 85, Update the vulnerable
dependencies in the POM: bump artifactId owasp-java-html-sanitizer to version
20260102.1 and add an explicit <scope>compile</scope> for the
owasp-java-html-sanitizer dependency; upgrade artifactId commons-text to 1.15.0
(or newer) to pull a safe commons-lang3, or alternatively add a
dependencyManagement override for groupId org.apache.commons / artifactId
commons-lang3 to at least 3.18.0 (preferably 3.20.0) to eliminate
CVE-2025-48924; locate the dependencies by their artifactId entries
(owasp-java-html-sanitizer and commons-text) and apply the version/scope changes
or add the commons-lang3 override in dependencyManagement.

</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();
Comment on lines +58 to +63

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Policy fields should be private static final.

POLICY_SOME_HTML, POLICY_NO_HTML use constant naming conventions but are package-private instance fields. They are immutable after construction and shared across all uses — they should be private static final. Same applies to HTML_TAGS on Line 139.

♻️ Proposed fix
-    PolicyFactory POLICY_SOME_HTML = new org.owasp.html.HtmlPolicyBuilder()
+    private static final 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 static final PolicyFactory POLICY_NO_HTML = new org.owasp.html.HtmlPolicyBuilder().toFactory();

And for Line 139:

-    Pattern HTML_TAGS = Pattern.compile("<[a-z]+[^>]*>");
+    private static final Pattern HTML_TAGS = Pattern.compile("<[a-z]+[^>]*>");
📝 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
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 static final PolicyFactory POLICY_SOME_HTML = new org.owasp.html.HtmlPolicyBuilder()
.allowElements(
"br", "p", "strong", "b"
).toFactory();
private static final PolicyFactory POLICY_NO_HTML = new org.owasp.html.HtmlPolicyBuilder().toFactory();
🤖 Prompt for AI Agents
In
`@misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java`
around lines 58 - 63, The fields POLICY_SOME_HTML and POLICY_NO_HTML (and
HTML_TAGS) are declared as package-private instance fields but are immutable
constants; change their declarations to be private static final and keep their
current initializers, e.g., update the declarations for POLICY_SOME_HTML and
POLICY_NO_HTML to "private static final PolicyFactory ..." and likewise make
HTML_TAGS a private static final (preserving its type and initializer) so they
are class-level constants.


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);
}
Comment on lines +73 to +80

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing English file causes an uncaught RuntimeException.

If the English .properties counterpart doesn't exist (e.g., new locale-only key file, or path transformation mismatch), this throws a RuntimeException that bypasses the MojoExecutionException handling in verify(). Consider adding the error to messages and returning early, or wrapping it in MojoExecutionException, consistent with the rest of the verification flow.

🛡️ Proposed fix — graceful fallback
         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);
+        File englishPath = new File(englishFile);
+        if (!englishPath.exists()) {
+            messages.add("English reference file not found: " + englishFile + " — skipping HTML safety check for " + file);
+            return;
+        }
+        try (FileInputStream fis = new FileInputStream(englishPath)) {
+            bundleEnglish = new PropertyResourceBundle(fis);
+        } catch (IOException e) {
+            messages.add("Unable to read English reference file " + englishFile + ": " + e.getMessage());
+            return;
         }
📝 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
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);
}
PropertyResourceBundle bundleEnglish;
String englishFile = file.getAbsolutePath().replaceAll("resources-community", "resources")
.replaceAll("_[a-zA-Z-_]*\\.properties", "_en.properties");
File englishPath = new File(englishFile);
if (!englishPath.exists()) {
messages.add("English reference file not found: " + englishFile + " — skipping HTML safety check for " + file);
return;
}
try (FileInputStream fis = new FileInputStream(englishPath)) {
bundleEnglish = new PropertyResourceBundle(fis);
} catch (IOException e) {
messages.add("Unable to read English reference file " + englishFile + ": " + e.getMessage());
return;
}
🤖 Prompt for AI Agents
In
`@misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java`
around lines 73 - 80, The code in VerifyMessageProperties that builds
englishFile and then loads it into bundleEnglish currently throws a
RuntimeException on IOException which escapes verify(); instead catch the
IOException around the FileInputStream/PropertyResourceBundle creation and
handle it like other verification errors: add a descriptive error entry to the
messages collection referencing the englishFile (or a message key) and return
early from verify(), or alternatively wrap the IOException in a
MojoExecutionException before rethrowing so the error follows the existing Mojo
flow; update the bundleEnglish usage to account for the early-return or wrapped
exception.


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 | 🟠 Major

Potential StringIndexOutOfBoundsException when prefix/suffix overlap.

If the matching prefix (start) and suffix (end) overlap (i.e., start + end >= sanitized.length()), sanitized.substring(start, sanitized.length() - end) will have beginIndex > endIndex, causing an exception. This can occur when the sanitizer strips content whose surrounding text characters coincidentally match.

🐛 Proposed fix — clamp end to avoid overlap
                 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++;
                 }
🤖 Prompt for AI Agents
In
`@misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java`
around lines 99 - 111, The diff can throw StringIndexOutOfBounds when the
computed prefix length (start) and suffix length (end) overlap; clamp end before
taking substrings in VerifyMessageProperties: compute end = Math.min(end,
Math.max(0, sanitized.length() - start), Math.max(0, value.length() - start))
(or equivalent) so sanitized.substring(start, sanitized.length() - end) and
value.substring(start, value.length() - end) never get beginIndex > endIndex,
then continue adding the message to messages using those safe substrings
(referencing variables start, end, sanitized, value, messages, key, file).

}

});
}

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;
}
Comment on lines +150 to +162

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: santizeAnchorssanitizeAnchors.

Also, the Matcher on Line 151 is bound to the original value string, but value is reassigned on Line 155 via replaceFirst. On subsequent loop iterations, matcher.find() continues scanning the old string while value has changed, causing position desync when multiple anchors exist.

Consider rebuilding the matcher after each replacement, or collecting all anchors first and validating them separately from the removal step.

🐛 Proposed fix — rename and rebuild matcher
-    private String santizeAnchors(String key, String value, String englishValue) {
-        Matcher matcher = ANCHOR_PATTERN.matcher(value);
+    private String sanitizeAnchors(String key, String value, String englishValue) {
         Matcher englishMatcher = ANCHOR_PATTERN.matcher(englishValue);
-        while (matcher.find()) {
-            if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) {
+        Matcher matcher = ANCHOR_PATTERN.matcher(value);
+        while (matcher.find()) {
+            if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) {
                 value = value.replaceFirst(Pattern.quote(englishMatcher.group()), "");
+                matcher = ANCHOR_PATTERN.matcher(value);
             } else {
                 messages.add("Didn't find anchor tag " + matcher.group() + " in original string");
                 break;
📝 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
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 String sanitizeAnchors(String key, String value, String englishValue) {
Matcher englishMatcher = ANCHOR_PATTERN.matcher(englishValue);
Matcher matcher = ANCHOR_PATTERN.matcher(value);
while (matcher.find()) {
if (englishMatcher.find() && Objects.equals(matcher.group(), englishMatcher.group())) {
value = value.replaceFirst(Pattern.quote(englishMatcher.group()), "");
matcher = ANCHOR_PATTERN.matcher(value);
} else {
messages.add("Didn't find anchor tag " + matcher.group() + " in original string");
break;
}
}
return value;
}
🤖 Prompt for AI Agents
In
`@misc/theme-verifier/src/main/java/org/keycloak/themeverifier/VerifyMessageProperties.java`
around lines 150 - 162, Rename the method from santizeAnchors to sanitizeAnchors
and fix the matcher desync by not mutating the string while iterating its
Matcher; either collect all anchor matches first via ANCHOR_PATTERN for both
value and englishValue and validate pairs, then perform removals, or after each
replaceFirst update the Matcher (recreate matcher =
ANCHOR_PATTERN.matcher(value)) and similarly reset englishMatcher when
appropriate; ensure you reference ANCHOR_PATTERN, matcher, englishMatcher,
replaceFirst and messages in the fix so anchors are validated against
englishValue before removal and the messages list is populated correctly.


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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Malformed closing tag </div — is this intentional?

The closing </div is missing the trailing >. If this is meant to test malformed HTML handling, consider adding a comment or a separate fixture for well-formed-but-disallowed HTML (<div>tag</div>) to ensure both cases are covered.

🤖 Prompt for AI Agents
In `@misc/theme-verifier/src/test/resources/illegalHtmlTag_en.properties` at line
17, The property value for the entry starting with "key=Some <div>tag</div" has
a malformed closing tag (it reads "</div" missing the trailing '>'); either
correct it to a well-formed disallowed tag ("key=Some <div>tag</div>") or, if
the intent is to test a malformed-tag case, add an explicit comment or separate
fixture entry (e.g., one file/value for malformed "</div" and another for the
well-formed "<div>tag</div>") so both scenarios are covered and unambiguous.

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.
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 | 🟠 Major

Wrong language: Italian text in Lithuanian (_lt) locale file.

The totpStep1 value is in Italian ("Installa una delle seguenti applicazioni sul tuo cellulare:") but this is the Lithuanian locale file. All surrounding messages (lines 100, 102–103) are in Lithuanian. This will result in Lithuanian users seeing an Italian instruction during the TOTP authenticator setup flow.

Restore the Lithuanian translation, e.g.:

Proposed fix
-totpStep1=Installa una delle seguenti applicazioni sul tuo cellulare:
+totpStep1=Įdiekite vieną iš šių programėlių į savo mobilųjį telefoną:
📝 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, The totpStep1 entry in the Lithuanian locale file currently
contains Italian text; open the messages_lt.properties and replace the totpStep1
value (key: totpStep1) with the correct Lithuanian translation, e.g. "Įdiekite
vieną iš šių programėlių savo mobiliajame telefone:", ensuring the string
matches surrounding Lithuanian entries and preserves any required property file
escaping/encoding.

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.

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

Traditional Chinese characters used in Simplified Chinese (zh_CN) locale file.

This line uses Traditional Chinese characters (機, 裝, 應用程式) whereas the entire file—and the zh_CN locale—uses Simplified Chinese. Compare with neighbouring lines 113–114 which correctly use Simplified forms (应用, 打开, 扫描). This will produce a jarring mix of scripts for mainland Chinese users.

🌐 Proposed fix: convert to Simplified Chinese
-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 value for the message key totpStep1 uses Traditional Chinese
characters; change the string to Simplified Chinese to match the zh_CN locale
and neighboring entries (e.g., replace "在您的手機上安裝以下應用程式之一:" with the Simplified
form "在您的手机上安装以下应用程序之一:" in the messages_zh_CN.properties file so the key
totpStep1 consistently uses Simplified characters).

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

Fix typo in localized sentence ("nHvis").

The paragraph starts with “nHvis”, which looks like an accidental leading character and will render incorrectly. Please correct to “Hvis”.

✏️ Suggested fix
-...<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>
+...<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, The localized property identityProviderLinkBodyHtml contains a stray
leading character "n" before "Hvis"; remove the unwanted "n" so the sentence
begins "Hvis" (i.e., change "nHvis du ikke vil..." to "Hvis du ikke vil...") to
correct the typo and ensure the HTML message renders properly.

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