Global security template for any plugin using Adventure's MiniMessage API. Copy this into every repository. Check it on every release.
When unfiltered player input reaches MiniMessage.deserialize(...), tags like these become live code:
| Tag | Risk |
|---|---|
<red> |
Unintended formatting |
<click:run_command:'/op ...'> |
UI manipulation |
<hover:show_text:'...'> |
Confusing chat behavior |
MiniMessage is safe by design β but only when trusted and untrusted data are kept strictly separate.
π Golden Rule: All player input is untrusted data. Always.
Apply these rules without exception in every plugin:
- Never deserialize raw player messages directly.
- Only trusted sources may contain MiniMessage markup β e.g. config files, hardcoded plugin strings, admin-defined templates.
- Always escape player input before embedding it:
MiniMessage.miniMessage().escapeTags(playerInput)
- If legacy color codes are allowed, convert them in a controlled, permission-gated way only.
- Treat PlaceholderAPI results as potentially untrusted whenever they may contain player-supplied text.
String raw = playerMessage; // untrusted!
String format = "<gray><name>: " + raw;
Component c = MiniMessage.miniMessage().deserialize(format); // UNSAFEPlayer input flows directly into deserialization β any tag they type gets executed.
MiniMessage mm = MiniMessage.miniMessage();
String trustedFormat = "<gray><name>: {message}"; // trusted source only
String safeMessage = mm.escapeTags(playerMessage); // sanitize untrusted input
String finalFormat = trustedFormat.replace("{message}", safeMessage);
Component c = mm.deserialize(finalFormat); // safeTrusted format and untrusted content are always kept separate.
MiniMessage mm = MiniMessage.miniMessage();
// 1. Extract plain text from the chat event
String raw = PlainTextComponentSerializer.plainText().serialize(event.message());
// 2. Always escape player input first
String safe = mm.escapeTags(raw);
// 3. Optionally allow legacy colors β only for players with explicit permission
if (player.hasPermission("plugin.chat.rgb")) {
safe = convertLegacyHexToMiniMessage(safe);
}
if (player.hasPermission("plugin.chat.color")) {
safe = convertLegacyStandardToMiniMessage(safe);
}
// 4. Load trusted format from config; inject sanitized message
String trustedFormat = loadFormatFromConfig();
String mmFormat = convertLegacyToMiniMessage(trustedFormat)
.replace("{message}", safe);
// 5. Render
Component out = mm.deserialize(mmFormat);
event.renderer((source, displayName, msg, viewer) -> out);Before merging or releasing any feature that handles player-supplied text:
- No direct
deserialize(playerInput)calls anywhere in the codebase - Every untrusted string is escaped before being passed to
deserialize - Trusted format strings and player content are clearly separated
- Legacy / RGB color conversion is permission-gated and scoped correctly
- PlaceholderAPI results have been reviewed β do any contain player input?
- Tag-injection test cases have been run and pass
Run these inputs through every chat, GUI, or text feature before release:
| # | Input | Expected Behavior |
|---|---|---|
| 1 | <red>Hello |
Tag rendered as plain text, not applied |
| 2 | <click:run_command:'/say hi'>Click |
Click event not registered |
| 3 | <hover:show_text:'X'>Text |
Hover event not registered |
| 4 | &aGreen |
Only works if legacy colors are enabled for that player |
| 5 | �FFAARGB |
Only works if RGB colors are enabled for that player |
| 6 | <red>&aTest<click:...> |
Mixed case β all tags neutralized |
Pass criteria:
- MiniMessage tags from player input are never executed
- Tags appear as literal text or are stripped entirely
- Only explicitly permitted legacy/RGB codes have any effect
Every new plugin in this organization must:
- Include this file in the repository root.
- Link to the security section from the main
README.md. - Check off the release checklist before every version tag.
- Test all chat / GUI / text features against the cases above.
Add this note to your code review template:
Untrusted input must never be parsed as MiniMessage tags.
For plugins that require the highest security posture:
- Disable MiniMessage entirely for player-facing text fields.
- Use plain text only, with server-side color definitions.
- Block messages containing detected tags and notify the sender.
Example player-facing message:
MiniMessage tags are not permitted in chat.
MiniMessage is powerful β keep it in trusted hands.