ghost 5.130.1 → 5.130.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. package/components/{tryghost-i18n-5.130.1.tgz → tryghost-i18n-5.130.3.tgz} +0 -0
  2. package/core/built/admin/assets/{chunk.524.8a4cbb5b8ae5cf01697e.js → chunk.524.d4dce2126c430c48812b.js} +8 -8
  3. package/core/built/admin/assets/{chunk.582.7b14e9ac2e84d285035e.js → chunk.582.7031dee8f98fced7de40.js} +9 -9
  4. package/core/built/admin/assets/{chunk.728.077782a432061228b91e.js → chunk.728.985c45ad584b4b91ca60.js} +19 -19
  5. package/core/built/admin/assets/{ghost-280b83af263b51bc4d6ce5bd8f536096.js → ghost-bf886dc6db7f0d5d41ebe6a468fadc90.js} +3 -3
  6. package/core/built/admin/assets/ghost-dark-a6901b9bab812b089d0c87e14735d69e.css +1 -0
  7. package/core/built/admin/assets/ghost-e474a058646b6df157de25ea1cd978d5.css +1 -0
  8. package/core/built/admin/index.html +4 -4
  9. package/core/server/services/explore-ping/ExplorePingService.js +3 -1
  10. package/core/server/services/members/members-api/controllers/RouterController.js +28 -10
  11. package/core/server/services/members/members-api/utils/normalize-email.js +31 -0
  12. package/core/server/services/settings/SettingsBREADService.js +5 -1
  13. package/core/server/services/settings/settings-service.js +2 -0
  14. package/package.json +4 -4
  15. package/tsconfig.tsbuildinfo +1 -1
  16. package/yarn.lock +4 -4
  17. package/core/built/admin/assets/ghost-3d0ad0c58f433d5735532bf25d4fd423.css +0 -1
  18. package/core/built/admin/assets/ghost-dark-f19869a3fd0ef48c525149b9c87e4241.css +0 -1
  19. /package/core/built/admin/assets/{chunk.728.077782a432061228b91e.js.LICENSE.txt → chunk.728.985c45ad584b4b91ca60.js.LICENSE.txt} +0 -0
@@ -28,7 +28,7 @@
28
28
  </style>
29
29
 
30
30
  <link integrity="" rel="stylesheet" href="assets/vendor-0ede59da8efb5e28fa929557f7ff7154.css">
31
- <link integrity="" rel="stylesheet" href="assets/ghost-3d0ad0c58f433d5735532bf25d4fd423.css" title="light">
31
+ <link integrity="" rel="stylesheet" href="assets/ghost-e474a058646b6df157de25ea1cd978d5.css" title="light">
32
32
 
33
33
 
34
34
  </head>
@@ -48,8 +48,8 @@
48
48
  <div id="ember-basic-dropdown-wormhole"></div>
49
49
 
50
50
  <script src="assets/vendor-aed0068cf9b67d042dd23a6343545b7b.js"></script>
51
- <script src="assets/chunk.728.077782a432061228b91e.js"></script>
52
- <script src="assets/chunk.524.8a4cbb5b8ae5cf01697e.js"></script>
53
- <script src="assets/ghost-280b83af263b51bc4d6ce5bd8f536096.js"></script>
51
+ <script src="assets/chunk.728.985c45ad584b4b91ca60.js"></script>
52
+ <script src="assets/chunk.524.d4dce2126c430c48812b.js"></script>
53
+ <script src="assets/ghost-bf886dc6db7f0d5d41ebe6a468fadc90.js"></script>
54
54
  </body>
55
55
  </html>
@@ -33,7 +33,9 @@ module.exports = class ExplorePingService {
33
33
  ghost: this.ghostVersion.full,
34
34
  site_uuid: this.settingsCache.get('site_uuid'),
35
35
  url: this.config.get('url'),
36
- theme: this.settingsCache.get('active_theme')
36
+ theme: this.settingsCache.get('active_theme'),
37
+ facebook: this.settingsCache.get('facebook'),
38
+ twitter: this.settingsCache.get('twitter')
37
39
  };
38
40
 
39
41
  try {
@@ -4,6 +4,7 @@ const sanitizeHtml = require('sanitize-html');
4
4
  const {BadRequestError, NoPermissionError, UnauthorizedError, DisabledFeatureError, NotFoundError} = require('@tryghost/errors');
5
5
  const errors = require('@tryghost/errors');
6
6
  const {isEmail} = require('@tryghost/validator');
7
+ const normalizeEmail = require('../utils/normalize-email');
7
8
 
8
9
  const messages = {
9
10
  emailRequired: 'Email is required.',
@@ -585,6 +586,23 @@ module.exports = class RouterController {
585
586
  });
586
587
  }
587
588
 
589
+ // Normalize email to prevent homograph attacks
590
+ let normalizedEmail;
591
+
592
+ try {
593
+ normalizedEmail = normalizeEmail(email);
594
+
595
+ if (normalizedEmail !== email) {
596
+ logging.info(`Email normalized from ${email} to ${normalizedEmail} for magic link`);
597
+ }
598
+ } catch (err) {
599
+ logging.error(`Failed to normalize [${email}]: ${err.message}`);
600
+
601
+ throw new errors.BadRequestError({
602
+ message: tpl(messages.invalidEmail)
603
+ });
604
+ }
605
+
588
606
  if (honeypot) {
589
607
  logging.warn('Honeypot field filled, this is likely a bot');
590
608
 
@@ -606,9 +624,9 @@ module.exports = class RouterController {
606
624
 
607
625
  try {
608
626
  if (emailType === 'signup' || emailType === 'subscribe') {
609
- await this._handleSignup(req, referrer);
627
+ await this._handleSignup(req, normalizedEmail, referrer);
610
628
  } else {
611
- await this._handleSignin(req, referrer);
629
+ await this._handleSignin(req, normalizedEmail, referrer);
612
630
  }
613
631
 
614
632
  res.writeHead(201);
@@ -626,7 +644,7 @@ module.exports = class RouterController {
626
644
  }
627
645
  }
628
646
 
629
- async _handleSignup(req, referrer = null) {
647
+ async _handleSignup(req, normalizedEmail, referrer = null) {
630
648
  if (!this._allowSelfSignup()) {
631
649
  if (this._settingsCache.get('members_signup_access') === 'paid') {
632
650
  throw new errors.BadRequestError({
@@ -640,14 +658,14 @@ module.exports = class RouterController {
640
658
  }
641
659
 
642
660
  const blockedEmailDomains = this._settingsCache.get('all_blocked_email_domains');
643
- const emailDomain = req.body.email.split('@')[1]?.toLowerCase();
661
+ const emailDomain = normalizedEmail.split('@')[1]?.toLowerCase();
644
662
  if (emailDomain && blockedEmailDomains.includes(emailDomain)) {
645
663
  throw new errors.BadRequestError({
646
664
  message: tpl(messages.blockedEmailDomain)
647
665
  });
648
666
  }
649
667
 
650
- const {email, emailType} = req.body;
668
+ const {emailType} = req.body;
651
669
 
652
670
  const tokenData = {
653
671
  labels: req.body.labels,
@@ -657,13 +675,13 @@ module.exports = class RouterController {
657
675
  attribution: await this._memberAttributionService.getAttribution(req.body.urlHistory)
658
676
  };
659
677
 
660
- return await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, referrer});
678
+ return await this._sendEmailWithMagicLink({email: normalizedEmail, tokenData, requestedType: emailType, referrer});
661
679
  }
662
680
 
663
- async _handleSignin(req, referrer = null) {
664
- const {email, emailType} = req.body;
681
+ async _handleSignin(req, normalizedEmail, referrer = null) {
682
+ const {emailType} = req.body;
665
683
 
666
- const member = await this._memberRepository.get({email});
684
+ const member = await this._memberRepository.get({email: normalizedEmail});
667
685
 
668
686
  if (!member) {
669
687
  throw new errors.BadRequestError({
@@ -672,7 +690,7 @@ module.exports = class RouterController {
672
690
  }
673
691
 
674
692
  const tokenData = {};
675
- return await this._sendEmailWithMagicLink({email, tokenData, requestedType: emailType, referrer});
693
+ return await this._sendEmailWithMagicLink({email: normalizedEmail, tokenData, requestedType: emailType, referrer});
676
694
  }
677
695
 
678
696
  /**
@@ -0,0 +1,31 @@
1
+ const punycode = require('punycode/');
2
+
3
+ /**
4
+ * Normalizes email addresses by converting Unicode domains to ASCII (punycode)
5
+ * This prevents homograph attacks where Unicode characters are used to spoof
6
+ * domains
7
+ *
8
+ * @param {string} email The email address to normalize
9
+ * @returns {string} The normalized email address
10
+ * @throws {Error} When punycode conversion fails
11
+ */
12
+ function normalizeEmail(email) {
13
+ if (!email || typeof email !== 'string') {
14
+ return null;
15
+ }
16
+
17
+ const atIndex = email.lastIndexOf('@');
18
+
19
+ if (atIndex === -1) {
20
+ return email;
21
+ }
22
+
23
+ const localPart = email.substring(0, atIndex);
24
+ const domainPart = email.substring(atIndex + 1);
25
+
26
+ const asciiDomain = punycode.toASCII(domainPart);
27
+
28
+ return `${localPart}@${asciiDomain}`;
29
+ }
30
+
31
+ module.exports = normalizeEmail;
@@ -24,12 +24,14 @@ class SettingsBREADService {
24
24
  * @param {Object} options.singleUseTokenProvider
25
25
  * @param {Object} options.urlUtils
26
26
  * @param {Object} options.labsService - labs service instance
27
+ * @param {Object} options.limitsService - limits service instance
27
28
  * @param {{service: Object}} options.emailAddressService
28
29
  */
29
- constructor({SettingsModel, settingsCache, labsService, mail, singleUseTokenProvider, urlUtils, emailAddressService}) {
30
+ constructor({SettingsModel, settingsCache, labsService, limitsService, mail, singleUseTokenProvider, urlUtils, emailAddressService}) {
30
31
  this.SettingsModel = SettingsModel;
31
32
  this.settingsCache = settingsCache;
32
33
  this.labs = labsService;
34
+ this.limitsService = limitsService;
33
35
  this.emailAddressService = emailAddressService;
34
36
 
35
37
  /* email verification setup */
@@ -194,6 +196,8 @@ class SettingsBREADService {
194
196
  }
195
197
 
196
198
  if (stripeConnectData) {
199
+ await this.limitsService.errorIfWouldGoOverLimit('limitStripeConnect');
200
+
197
201
  filteredSettings.push({
198
202
  key: 'stripe_connect_publishable_key',
199
203
  value: stripeConnectData.public_key
@@ -5,6 +5,7 @@
5
5
  const events = require('../../lib/common/events');
6
6
  const models = require('../../models');
7
7
  const labs = require('../../../shared/labs');
8
+ const limits = require('../limits');
8
9
  const config = require('../../../shared/config');
9
10
  const adapterManager = require('../adapter-manager');
10
11
  const SettingsCache = require('../../../shared/settings-cache');
@@ -30,6 +31,7 @@ const getSettingsBREADServiceInstance = () => {
30
31
  SettingsModel: models.Settings,
31
32
  settingsCache: SettingsCache,
32
33
  labsService: labs,
34
+ limitsService: limits,
33
35
  mail,
34
36
  singleUseTokenProvider: new SingleUseTokenProvider({
35
37
  SingleUseTokenModel: models.SingleUseToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "5.130.1",
3
+ "version": "5.130.3",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -86,7 +86,7 @@
86
86
  "@tryghost/helpers": "1.1.97",
87
87
  "@tryghost/html-to-plaintext": "1.0.4",
88
88
  "@tryghost/http-cache-utils": "0.1.20",
89
- "@tryghost/i18n": "file:components/tryghost-i18n-5.130.1.tgz",
89
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.130.3.tgz",
90
90
  "@tryghost/image-transform": "1.4.6",
91
91
  "@tryghost/job-manager": "1.0.3",
92
92
  "@tryghost/kg-card-factory": "5.1.2",
@@ -233,7 +233,7 @@
233
233
  "@types/bookshelf": "1.2.9",
234
234
  "@types/common-tags": "1.8.4",
235
235
  "@types/jsonwebtoken": "9.0.10",
236
- "@types/node": "22.16.4",
236
+ "@types/node": "22.16.5",
237
237
  "@types/node-jose": "1.1.13",
238
238
  "@types/nodemailer": "6.4.17",
239
239
  "@types/sinon": "17.0.4",
@@ -274,7 +274,7 @@
274
274
  "jackspeak": "2.3.6",
275
275
  "moment": "2.24.0",
276
276
  "moment-timezone": "0.5.45",
277
- "@tryghost/i18n": "file:components/tryghost-i18n-5.130.1.tgz"
277
+ "@tryghost/i18n": "file:components/tryghost-i18n-5.130.3.tgz"
278
278
  },
279
279
  "nx": {
280
280
  "targets": {