diff --git a/banned.html b/banned.html
index 658f5b44..e1d0e2d5 100644
--- a/banned.html
+++ b/banned.html
@@ -137,8 +137,13 @@
This account is banned
reasonEl.textContent = `Reason: ${reason}`;
}
- if (appeal && /^https:\/\//i.test(appeal)) {
- linkEl.href = appeal;
+ if (appeal) {
+ // Only allow passing a Discord invite code, not a full URL, to avoid open-redirects.
+ // Example: ?appeal=rKgF9s32EV
+ const inviteCode = appeal.replace(/^https?:\/\/(www\.)?discord\.gg\//i, '').trim();
+ if (/^[A-Za-z0-9-]{2,64}$/.test(inviteCode)) {
+ linkEl.href = `https://discord.gg/${inviteCode}`;
+ }
}
})();
diff --git a/index.html b/index.html
index 95f2eba5..1da0c349 100644
--- a/index.html
+++ b/index.html
@@ -13,7 +13,7 @@
-
+
diff --git a/learn/ui/js/services/trialMode.js b/learn/ui/js/services/trialMode.js
index 61d0e938..39196fba 100644
--- a/learn/ui/js/services/trialMode.js
+++ b/learn/ui/js/services/trialMode.js
@@ -126,7 +126,15 @@ export function clearTrialSession() {
}
function generateSessionId() {
- return `trial_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
+ const cryptoObj = globalThis.crypto || globalThis.msCrypto;
+ if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function') {
+ throw new Error('Secure randomness unavailable (crypto.getRandomValues)');
+ }
+
+ const bytes = new Uint8Array(16);
+ cryptoObj.getRandomValues(bytes);
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
+ return `trial_${Date.now()}_${hex}`;
}
export function getTrialStats() {