diff --git a/server-ce/hotfix/4.2.8/Dockerfile b/server-ce/hotfix/4.2.8/Dockerfile
new file mode 100644
index 0000000000..a2dbe4f3b5
--- /dev/null
+++ b/server-ce/hotfix/4.2.8/Dockerfile
@@ -0,0 +1,11 @@
+FROM sharelatex/sharelatex:4.2.7
+
+# Fix crash on on invalid URLs
+COPY pr_19612.patch .
+RUN patch -p1 < pr_19612.patch && rm pr_19612.patch
+
+COPY pr_19550.patch .
+RUN patch -p1 < pr_19550.patch && rm pr_19550.patch
+
+COPY is_19575.patch /etc/nginx/sites-enabled/
+RUN cd /etc/nginx/sites-enabled && patch -p0 < is_19575.patch && rm is_19575.patch
diff --git a/server-ce/hotfix/4.2.8/is_19575.patch b/server-ce/hotfix/4.2.8/is_19575.patch
new file mode 100644
index 0000000000..5854d85234
--- /dev/null
+++ b/server-ce/hotfix/4.2.8/is_19575.patch
@@ -0,0 +1,19 @@
+--- sharelatex.conf
++++ sharelatex.conf
+@@ -67,6 +67,16 @@ server {
+ proxy_http_version 1.1;
+ }
+
++ # block external access to metrics
++ location ~* ^/metrics/?$ {
++ return 404 'Not found';
++ }
++
++ # block external access to all health checks /health_check, /health_check/full, etc
++ location ~* ^/health_check {
++ return 404 'Not found';
++ }
++
+ # Load any extra configuration for this vhost
+ include /etc/nginx/vhost-extras/overleaf/*.conf;
+ }
diff --git a/server-ce/hotfix/4.2.8/pr_19550.patch b/server-ce/hotfix/4.2.8/pr_19550.patch
new file mode 100644
index 0000000000..484a3f2980
--- /dev/null
+++ b/server-ce/hotfix/4.2.8/pr_19550.patch
@@ -0,0 +1,58 @@
+diff --git a/services/web/app/src/infrastructure/CSP.js b/services/web/app/src/infrastructure/CSP.js
+index 28f4f380d3d..abc11c59a48 100644
+--- a/services/web/app/src/infrastructure/CSP.js
++++ b/services/web/app/src/infrastructure/CSP.js
+@@ -6,6 +6,7 @@ module.exports = function ({
+ reportPercentage,
+ reportOnly = false,
+ exclude = [],
++ viewDirectives = {},
+ }) {
+ const header = reportOnly
+ ? 'Content-Security-Policy-Report-Only'
+@@ -33,7 +34,12 @@ module.exports = function ({
+
+ res.locals.scriptNonce = scriptNonce
+
+- const policy = buildViewPolicy(scriptNonce, reportPercentage, reportUri)
++ const policy = buildViewPolicy(
++ scriptNonce,
++ reportPercentage,
++ reportUri,
++ viewDirectives[view]
++ )
+
+ // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
+
+@@ -68,11 +74,17 @@ const buildDefaultPolicy = (reportUri, styleSrc) => {
+ return directives.join('; ')
+ }
+
+-const buildViewPolicy = (scriptNonce, reportPercentage, reportUri) => {
++const buildViewPolicy = (
++ scriptNonce,
++ reportPercentage,
++ reportUri,
++ viewDirectives
++) => {
+ const directives = [
+ `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, // only allow scripts from certain sources
+ `object-src 'none'`, // forbid loading an "object" element
+ `base-uri 'none'`, // forbid setting a "base" element
++ ...(viewDirectives ?? []),
+ ]
+
+ if (reportUri) {
+--- a/services/web/config/settings.defaults.js
++++ b/services/web/config/settings.defaults.js
+@@ -868,6 +868,9 @@ module.exports = {
+ reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
+ reportUri: process.env.CSP_REPORT_URI,
+ exclude: ['app/views/project/editor'],
++ viewDirectives: {
++ 'app/views/project/ide-react': [`img-src 'self' data: blob:`],
++ },
+ },
+
+ unsupportedBrowsers: {
+
diff --git a/server-ce/hotfix/4.2.8/pr_19612.patch b/server-ce/hotfix/4.2.8/pr_19612.patch
new file mode 100644
index 0000000000..3f46f92f90
--- /dev/null
+++ b/server-ce/hotfix/4.2.8/pr_19612.patch
@@ -0,0 +1,46 @@
+diff --git a/services/web/app/src/Features/HealthCheck/HealthCheckController.js b/services/web/app/src/Features/HealthCheck/HealthCheckController.js
+index 278f04bb767..ff074cfa816 100644
+--- a/services/web/app/src/Features/HealthCheck/HealthCheckController.js
++++ b/services/web/app/src/Features/HealthCheck/HealthCheckController.js
+@@ -45,6 +45,10 @@ module.exports = {
+ logger.err({ err }, 'failed api redis health check')
+ return res.sendStatus(500)
+ }
++ if (!settings.smokeTest.userId) {
++ logger.err({}, 'smokeTest.userId is undefined in health check')
++ return res.sendStatus(404)
++ }
+ UserGetter.getUserEmail(settings.smokeTest.userId, (err, email) => {
+ if (err) {
+ logger.err({ err }, 'failed api mongo health check')
+diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js
+index 5f14977d3a3..2e9ed4f1ebb 100644
+--- a/services/web/app/src/infrastructure/ExpressLocals.js
++++ b/services/web/app/src/infrastructure/ExpressLocals.js
+@@ -11,6 +11,7 @@ const Features = require('./Features')
+ const SessionManager = require('../Features/Authentication/SessionManager')
+ const PackageVersions = require('./PackageVersions')
+ const Modules = require('./Modules')
++const Errors = require('../Features/Errors/Errors')
+ const {
+ canRedirectToAdminDomain,
+ hasAdminAccess,
+@@ -236,10 +237,14 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
+
+ // Don't include the query string parameters, otherwise Google
+ // treats ?nocdn=true as the canonical version
+- const parsedOriginalUrl = new URL(req.originalUrl, Settings.siteUrl)
+- res.locals.currentUrl = parsedOriginalUrl.pathname
+- res.locals.currentUrlWithQueryParams =
+- parsedOriginalUrl.pathname + parsedOriginalUrl.search
++ try {
++ const parsedOriginalUrl = new URL(req.originalUrl, Settings.siteUrl)
++ res.locals.currentUrl = parsedOriginalUrl.pathname
++ res.locals.currentUrlWithQueryParams =
++ parsedOriginalUrl.pathname + parsedOriginalUrl.search
++ } catch (err) {
++ return next(new Errors.InvalidError())
++ }
+ res.locals.capitalize = function (string) {
+ if (string.length === 0) {
+ return ''
diff --git a/server-ce/hotfix/5.1.1/Dockerfile b/server-ce/hotfix/5.1.1/Dockerfile
new file mode 100644
index 0000000000..5936808262
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/Dockerfile
@@ -0,0 +1,20 @@
+FROM sharelatex/sharelatex:5.1.0
+
+# Confirmation email fix
+COPY pr_19676.patch .
+RUN patch -p1 < pr_19676.patch && rm pr_19676.patch
+
+# Fix crash on on invalid URLs
+COPY pr_19612.patch .
+RUN patch -p1 < pr_19612.patch && rm pr_19612.patch
+
+# Remove Editor Resources check from launchpad
+COPY pr_19543.patch .
+RUN patch -p1 < pr_19543.patch && rm pr_19543.patch
+
+COPY pr_19550.patch .
+RUN patch -p1 < pr_19550.patch && rm pr_19550.patch
+
+COPY is_19575.patch /etc/nginx/sites-enabled/
+RUN cd /etc/nginx/sites-enabled && patch -p0 < is_19575.patch && rm is_19575.patch
+
diff --git a/server-ce/hotfix/5.1.1/is_19575.patch b/server-ce/hotfix/5.1.1/is_19575.patch
new file mode 100644
index 0000000000..ca78ae50d6
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/is_19575.patch
@@ -0,0 +1,19 @@
+--- overleaf.conf
++++ overleaf.conf
+@@ -67,6 +67,16 @@ server {
+ proxy_http_version 1.1;
+ }
+
++ # block external access to metrics
++ location ~* ^/metrics/?$ {
++ return 404 'Not found';
++ }
++
++ # block external access to all health checks /health_check, /health_check/full, etc
++ location ~* ^/health_check {
++ return 404 'Not found';
++ }
++
+ # Load any extra configuration for this vhost
+ include /etc/nginx/vhost-extras/overleaf/*.conf;
+ }
diff --git a/server-ce/hotfix/5.1.1/pr_19543.patch b/server-ce/hotfix/5.1.1/pr_19543.patch
new file mode 100644
index 0000000000..77e2b1152f
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/pr_19543.patch
@@ -0,0 +1,30 @@
+diff --git a/services/web/locales/en.json b/services/web/locales/en.json
+index a953a01a1d7..13e20b37279 100644
+--- a/services/web/locales/en.json
++++ b/services/web/locales/en.json
+@@ -519,7 +519,6 @@
+ "editor_disconected_click_to_reconnect": "Editor disconnected, click anywhere to reconnect.",
+ "editor_limit_exceeded_in_this_project": "Too many editors in this project",
+ "editor_only_hide_pdf": "Editor only <0>(hide PDF)0>",
+- "editor_resources": "Editor Resources",
+ "editor_theme": "Editor theme",
+ "educational_discount_applied": "40% educational discount applied!",
+ "educational_discount_available_for_groups_of_ten_or_more": "The educational discount is available for groups of 10 or more",
+diff --git a/services/web/modules/launchpad/app/views/launchpad.pug b/services/web/modules/launchpad/app/views/launchpad.pug
+index c478fe7b649..28d3ff8fc83 100644
+--- a/services/web/modules/launchpad/app/views/launchpad.pug
++++ b/services/web/modules/launchpad/app/views/launchpad.pug
+@@ -166,13 +166,6 @@ block content
+
+ h2 #{translate('status_checks')}
+
+-
+- .row.row-spaced-small
+- .col-sm-5
+- | #{translate('editor_resources')}
+- .col-sm-7
+- +launchpad-check('ide')
+-
+
+ .row.row-spaced-small
+ .col-sm-5
diff --git a/server-ce/hotfix/5.1.1/pr_19550.patch b/server-ce/hotfix/5.1.1/pr_19550.patch
new file mode 100644
index 0000000000..f52a5dd7e1
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/pr_19550.patch
@@ -0,0 +1,59 @@
+diff --git a/services/web/app/src/infrastructure/CSP.js b/services/web/app/src/infrastructure/CSP.js
+index 28f4f380d3d..abc11c59a48 100644
+--- a/services/web/app/src/infrastructure/CSP.js
++++ b/services/web/app/src/infrastructure/CSP.js
+@@ -6,6 +6,7 @@ module.exports = function ({
+ reportPercentage,
+ reportOnly = false,
+ exclude = [],
++ viewDirectives = {},
+ }) {
+ const header = reportOnly
+ ? 'Content-Security-Policy-Report-Only'
+@@ -33,7 +34,12 @@ module.exports = function ({
+
+ res.locals.scriptNonce = scriptNonce
+
+- const policy = buildViewPolicy(scriptNonce, reportPercentage, reportUri)
++ const policy = buildViewPolicy(
++ scriptNonce,
++ reportPercentage,
++ reportUri,
++ viewDirectives[view]
++ )
+
+ // Note: https://csp-evaluator.withgoogle.com/ is useful for checking the policy
+
+@@ -68,11 +74,17 @@ const buildDefaultPolicy = (reportUri, styleSrc) => {
+ return directives.join('; ')
+ }
+
+-const buildViewPolicy = (scriptNonce, reportPercentage, reportUri) => {
++const buildViewPolicy = (
++ scriptNonce,
++ reportPercentage,
++ reportUri,
++ viewDirectives
++) => {
+ const directives = [
+ `script-src 'nonce-${scriptNonce}' 'unsafe-inline' 'strict-dynamic' https: 'report-sample'`, // only allow scripts from certain sources
+ `object-src 'none'`, // forbid loading an "object" element
+ `base-uri 'none'`, // forbid setting a "base" element
++ ...(viewDirectives ?? []),
+ ]
+
+ if (reportUri) {
+diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js
+index cad13ab8156..ab738babdcd 100644
+--- a/services/web/config/settings.defaults.js
++++ b/services/web/config/settings.defaults.js
+@@ -911,6 +911,9 @@ module.exports = {
+ reportPercentage: parseFloat(process.env.CSP_REPORT_PERCENTAGE) || 0,
+ reportUri: process.env.CSP_REPORT_URI,
+ exclude: [],
++ viewDirectives: {
++ 'app/views/project/ide-react': [`img-src 'self' data: blob:`],
++ },
+ },
+
+ unsupportedBrowsers: {
diff --git a/server-ce/hotfix/5.1.1/pr_19612.patch b/server-ce/hotfix/5.1.1/pr_19612.patch
new file mode 100644
index 0000000000..3f46f92f90
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/pr_19612.patch
@@ -0,0 +1,46 @@
+diff --git a/services/web/app/src/Features/HealthCheck/HealthCheckController.js b/services/web/app/src/Features/HealthCheck/HealthCheckController.js
+index 278f04bb767..ff074cfa816 100644
+--- a/services/web/app/src/Features/HealthCheck/HealthCheckController.js
++++ b/services/web/app/src/Features/HealthCheck/HealthCheckController.js
+@@ -45,6 +45,10 @@ module.exports = {
+ logger.err({ err }, 'failed api redis health check')
+ return res.sendStatus(500)
+ }
++ if (!settings.smokeTest.userId) {
++ logger.err({}, 'smokeTest.userId is undefined in health check')
++ return res.sendStatus(404)
++ }
+ UserGetter.getUserEmail(settings.smokeTest.userId, (err, email) => {
+ if (err) {
+ logger.err({ err }, 'failed api mongo health check')
+diff --git a/services/web/app/src/infrastructure/ExpressLocals.js b/services/web/app/src/infrastructure/ExpressLocals.js
+index 5f14977d3a3..2e9ed4f1ebb 100644
+--- a/services/web/app/src/infrastructure/ExpressLocals.js
++++ b/services/web/app/src/infrastructure/ExpressLocals.js
+@@ -11,6 +11,7 @@ const Features = require('./Features')
+ const SessionManager = require('../Features/Authentication/SessionManager')
+ const PackageVersions = require('./PackageVersions')
+ const Modules = require('./Modules')
++const Errors = require('../Features/Errors/Errors')
+ const {
+ canRedirectToAdminDomain,
+ hasAdminAccess,
+@@ -236,10 +237,14 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
+
+ // Don't include the query string parameters, otherwise Google
+ // treats ?nocdn=true as the canonical version
+- const parsedOriginalUrl = new URL(req.originalUrl, Settings.siteUrl)
+- res.locals.currentUrl = parsedOriginalUrl.pathname
+- res.locals.currentUrlWithQueryParams =
+- parsedOriginalUrl.pathname + parsedOriginalUrl.search
++ try {
++ const parsedOriginalUrl = new URL(req.originalUrl, Settings.siteUrl)
++ res.locals.currentUrl = parsedOriginalUrl.pathname
++ res.locals.currentUrlWithQueryParams =
++ parsedOriginalUrl.pathname + parsedOriginalUrl.search
++ } catch (err) {
++ return next(new Errors.InvalidError())
++ }
+ res.locals.capitalize = function (string) {
+ if (string.length === 0) {
+ return ''
diff --git a/server-ce/hotfix/5.1.1/pr_19676.patch b/server-ce/hotfix/5.1.1/pr_19676.patch
new file mode 100644
index 0000000000..287ee78376
--- /dev/null
+++ b/server-ce/hotfix/5.1.1/pr_19676.patch
@@ -0,0 +1,26 @@
+diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js
+index 46d014a8e14..d839d67f634 100644
+--- a/services/web/app/src/Features/Email/EmailBuilder.js
++++ b/services/web/app/src/Features/Email/EmailBuilder.js
+@@ -234,7 +234,7 @@ templates.confirmEmail = ctaTemplate({
+ },
+ secondaryMessage() {
+ return [
+- 'If you did not request this, please let us know at support@overleaf.com.',
++ `If you did not request this, please let us know at ${settings.adminEmail}.`,
+ `If you have any questions or trouble confirming your email address, please get in touch with our support team at ${settings.adminEmail}.`,
+ ]
+ },
+diff --git a/services/web/app/src/Features/User/UserRegistrationHandler.js b/services/web/app/src/Features/User/UserRegistrationHandler.js
+index 2802fdc81c5..02c52f73fd2 100644
+--- a/services/web/app/src/Features/User/UserRegistrationHandler.js
++++ b/services/web/app/src/Features/User/UserRegistrationHandler.js
+@@ -113,7 +113,7 @@ const UserRegistrationHandler = {
+
+ const setNewPasswordUrl = `${settings.siteUrl}/user/activate?token=${token}&user_id=${user._id}`
+
+- EmailHandler.promises
++ await EmailHandler.promises
+ .sendEmail('registered', {
+ to: user.email,
+ setNewPasswordUrl,
diff --git a/server-ce/nginx/overleaf.conf b/server-ce/nginx/overleaf.conf
index a860087087..78af603c1e 100644
--- a/server-ce/nginx/overleaf.conf
+++ b/server-ce/nginx/overleaf.conf
@@ -67,6 +67,16 @@ server {
proxy_http_version 1.1;
}
+ # block external access to metrics
+ location ~* ^/metrics/?$ {
+ return 404 'Not found';
+ }
+
+ # block external access to all health checks /health_check, /health_check/full, etc
+ location ~* ^/health_check {
+ return 404 'Not found';
+ }
+
# Load any extra configuration for this vhost
include /etc/nginx/vhost-extras/overleaf/*.conf;
}