Skip to content

feat: security hardening — message validator, rate limiter, key loader#720

Open
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:feat/pr1-security-hardening
Open

feat: security hardening — message validator, rate limiter, key loader#720
Z0mb13V1 wants to merge 1 commit intomindcraft-bots:developfrom
Z0mb13V1:feat/pr1-security-hardening

Conversation

@Z0mb13V1
Copy link

@Z0mb13V1 Z0mb13V1 commented Mar 4, 2026

Security Hardening

Adds three security utility modules that close several known attack vectors:

Changes

  • *\src/utils/message_validator.js* — Input validation with command injection detection, type enforcement, control character stripping, and maximum-length guard
  • *\src/utils/rate_limiter.js* — Per-user rate limiting with automatic stale-entry cleanup to prevent abuse and memory leaks
  • *\src/utils/keys.js* — Environment-variable-first key loading (env vars always override \keys.json), enabling secure deployment without committing secrets

Security issues resolved

  • Prototype pollution surface on external config input
  • Command injection via unvalidated chat messages
  • API key exposure via commited \keys.json\
  • Unbounded request rates from single users

Testing

All changes are pure utility modules with no external dependencies beyond Node built-ins. Existing behavior is preserved.

- src/utils/message_validator.js: command injection detection, type checks,
  control character stripping, max-length guard
- src/utils/rate_limiter.js: per-user rate limiting with auto stale-entry cleanup
- src/utils/keys.js: env-first key loading (env vars always override keys.json)

Resolves prototype pollution, injection, and abuse vectors.
Copilot AI review requested due to automatic review settings March 4, 2026 00:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds security-oriented utility modules for validating inbound messages, rate-limiting per user, and loading API keys with environment-variable precedence to reduce common abuse/exposure risks.

Changes:

  • Added an in-memory per-user RateLimiter with periodic stale-entry cleanup.
  • Added Discord/Minecraft message and username validation + sanitization helpers.
  • Updated key loading to prefer environment variables and warn on keys.json fallback, with attempted sanitization of parsed JSON.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/utils/rate_limiter.js New per-user in-memory rate limiter with windowing and automatic cleanup.
src/utils/message_validator.js New validators/sanitizers for Discord/Minecraft messages and usernames.
src/utils/keys.js Changes key loading to env-first with keys.json fallback + warnings and JSON sanitization attempt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +66
export function validateMinecraftMessage(message) {
if (!message) return { valid: false, error: 'Empty message' };
if (typeof message !== 'string') return { valid: false, error: 'Message must be a string' };
if (message.length > MAX_MINECRAFT_MESSAGE_LENGTH) {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

Same issue as validateDiscordMessage: !message runs before the type check, so non-string falsy values produce "Empty message" rather than a type error. Type-check first, then validate emptiness.

Copilot uses AI. Check for mistakes.
Comment on lines +91 to +92
if (!username) return { valid: false, error: 'Username is empty' };
if (typeof username !== 'string') return { valid: false, error: 'Username must be a string' };
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

validateUsername checks !username before validating the type, so non-string falsy values return "Username is empty" rather than "Username must be a string". Validate typeof username === 'string' first, then check for empty/length constraints.

Suggested change
if (!username) return { valid: false, error: 'Username is empty' };
if (typeof username !== 'string') return { valid: false, error: 'Username must be a string' };
if (typeof username !== 'string') return { valid: false, error: 'Username must be a string' };
if (username.length === 0) return { valid: false, error: 'Username is empty' };

Copilot uses AI. Check for mistakes.
Comment on lines +2 to 3
import { deepSanitize } from '../../settings.js';

Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

deepSanitize is imported from ../../settings.js, but settings.js only exports a default settings object and does not provide a named deepSanitize export. This will throw at module load time and break every model that imports getKey. Define/export deepSanitize (or import it from the correct module) before using it here.

Suggested change
import { deepSanitize } from '../../settings.js';
// Local deepSanitize implementation to avoid relying on a non-existent named export.
// Currently acts as a no-op passthrough; adjust if stricter sanitization is needed.
function deepSanitize(value) {
return value;
}

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +16
} catch (_err) {
// keys.json not found or unreadable — that's fine, use env vars
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

loadKeysFile() catches and ignores all errors (including JSON parse errors or permission issues) and then sets keysLoaded = true. This can mask a malformed/unreadable keys.json and later surface as a misleading "key not found" error. Consider only ignoring ENOENT (missing file) and logging/rethrowing for other error types (including SyntaxError from JSON.parse).

Suggested change
} catch (_err) {
// keys.json not found or unreadable — that's fine, use env vars
} catch (err) {
// Only ignore missing file; other errors should be surfaced
if (!err || err.code !== 'ENOENT') {
console.error('Failed to load keys.json:', err);
throw err;
}

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +25
export function validateDiscordMessage(message) {
if (!message) return { valid: false, error: 'Empty message' };
if (typeof message !== 'string') return { valid: false, error: 'Message must be a string' };
if (message.length > MAX_DISCORD_MESSAGE_LENGTH) {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

The !message check runs before the typeof message !== 'string' check, so non-string falsy values (e.g. 0, false) will return the misleading error "Empty message" instead of "Message must be a string". Swap the order (type-check first) or explicitly check for empty string after validating the type.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants