Implement Content-Security-Policy (#15567)

* feat(security): implement CSP

* fix(csp): update helmet version to latest

* Squashed commit of the following:

commit cc6a35e61db07759c1f32716185543bc48bce760
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:27:50 2025 -0600

    fix(CSP): more Amazon domains

commit 985b86c29af866b2df942c21217d99390a2c6e92
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:18:08 2025 -0600

    fix(csp): more loggly allowance

commit 166bd315272f9c3a42652f3a026c27a88ed1a549
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:12:00 2025 -0600

    fix(csp): data, inline, some refactoring

commit 1a0a6c1806a53d43a7199bb2ef72cff610e908be
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 17:05:44 2025 -0600

    fix(CSP): override default script-src

commit 023d9886c835989da9c5901c168d66b572097dcf
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:56:24 2025 -0600

    fix(CSP): unsafe-eval in default-src

commit f51f0a0c93b60dfec7ce02be0ecd2587fc882fe0
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:52:14 2025 -0600

    fix(CSP): move trusted list to default-src

commit 83b2ba7688dea38abb651cf5c27482a7a3648374
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:38:05 2025 -0600

    fix(CSP): explicit habitica/aws in script-src

commit d5ca5172d5ad2fd8cec9402d7d2c9452c6ece7a1
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:31:38 2025 -0600

    fix(CSP): need escaped single quotes

commit c677a1ffeff5793b6da228924e68d2c2e47794b2
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:27:46 2025 -0600

    fix(CSP): unsafe-eval

commit 6ef35c3f7281c8426d9c333686be6bb65f00b3a8
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 16:15:07 2025 -0600

    fix(CSP): might need to skip entirely in dev but try no 'self'

commit 5759fb37d82fa61b474f01e9ce5e2dc461f6ceba
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 12 15:51:26 2025 -0600

    fix(csp): permit AWS in default-src

commit 9f238abf9373bc29715657b945b2247ef23c9224
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 5 17:22:25 2025 -0600

    fix(csp): update helmet version to latest

commit 9462e90f4f3058f4014137b3178b9751c5280e97
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Nov 25 09:27:05 2025 -0600

    feat(security): implement CSP

commit 72539f9ba3
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:53 2025 -0600

    5.42.2

commit dabd466719
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:16:48 2025 -0600

    Revert "Chat optimization (#15545)"

    This reverts commit 2917955ef0.

commit 8bf2304330
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Dec 10 14:15:48 2025 -0600

    chore(event): G1G1 date tweaks

commit 6937dc4e4e
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 8 16:37:04 2025 -0600

    fix(subscription): couple more layout tweaks

* fix(csp): move unsafe-eval to default? ig?

* Revert "fix(csp): move unsafe-eval to default? ig?"

This reverts commit 90476cbf6c59132024d4fbaeceb7cdb19a876303.

* fix(security): no unsafe! yay!

* fix(packages): remove webpack

* fix(lint): object destructuring

* fix(csp): remove Vue-Fragment

* wip(i18n): load Moment locale from cache

* fix(gulp): remove unneeded cache task

* fix(i18n): add Moment weekday abbrevs to translations

* fix(lint): destructuring
...why is this happening here and not develop lol

* fix(csp): add amplitude to whitelist

---------

Co-authored-by: Phillip Thelen <phillip@thelen.space>
This commit is contained in:
Kalista Payne
2026-04-02 15:19:35 -05:00
committed by GitHub
parent 3d3db1bdd9
commit 2ee2b05d1c
37 changed files with 257 additions and 856 deletions
+4
View File
@@ -16,6 +16,10 @@
border-bottom: 0;
}
.d-content {
display: contents;
}
* {
transition: none;
}
@@ -108,15 +108,15 @@ export default {
const allEmails = [];
if (user.auth.local.email) allEmails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.emails) {
const emails = user.auth.google.emails;
const { emails } = user.auth.google;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.apple && user.auth.apple.emails) {
const emails = user.auth.apple.emails;
const { emails } = user.auth.apple;
allEmails.push(...this.findSocialEmails(emails));
}
if (user.auth.facebook && user.auth.facebook.emails) {
const emails = user.auth.facebook.emails;
const { emails } = user.auth.facebook;
allEmails.push(...this.findSocialEmails(emails));
}
return allEmails;
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner';
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
const humanReadablePaymentDetails = {
customerId: {
@@ -189,7 +189,7 @@ export default {
this.cancel();
return [];
}
this.currentSearch = regexRes[1];
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
if (this.currentSearch.length === 0) return [];
@@ -470,7 +470,7 @@ export default {
return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false;
const purchased = group.purchased;
const { purchased } = group;
if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false;
@@ -492,7 +492,7 @@ export default {
},
isPartyPreviouslyUpgraded () {
if (!this.userParty) return false;
const purchased = this.userParty.purchased;
const { purchased } = this.userParty;
if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
@@ -533,7 +533,7 @@ export default {
this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) {
this.selectedOption = this.upgradeableGuilds[0];
[this.selectedOption] = this.upgradeableGuilds;
} else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty;
} else {
@@ -1399,7 +1399,7 @@ export default {
this.task.down = !this.task.down;
},
weekdaysMin (dayNumber) {
return moment.weekdaysMin(dayNumber);
return this.$t(`weekdaysMin${dayNumber}`);
},
formattedDate (date) {
return moment(date).format('MM/DD/YYYY');
+1 -1
View File
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
import Vue from 'vue';
import getStore from '@/store';
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const { AMPLITUDE_KEY } = import.meta.env;
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false;
-18
View File
@@ -1,28 +1,10 @@
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
import moment from 'moment';
import i18n from '@/../../common/script/i18n';
function loadLocale (i18nData) {
// Load i18n strings
i18n.strings = i18nData.strings;
// Load Moment.js locale
const { language } = i18nData;
if (language && i18nData.momentLang && language.momentLangCode) {
// Make moment available under `window` so that the locale can be set
window.moment = moment;
// Execute the script and set the locale
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.type = 'text/javascript';
script.text = i18nData.momentLang;
head.appendChild(script);
moment.updateLocale(language.momentLangCode);
}
}
export default {
-2
View File
@@ -11,7 +11,6 @@ import {
NavbarPlugin,
CollapsePlugin,
} from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app';
import { setUpLogging } from '@/libs/logging';
import router from './router/index';
@@ -44,7 +43,6 @@ Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin);
Vue.use(Fragment.Plugin);
setUpLogging();
const store = getStore();
+1 -1
View File
@@ -8,7 +8,7 @@ import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
const { STRIPE_PUB_KEY } = import.meta.env;
let stripeInstance = null;
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -90,7 +90,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment v-if="allowedToChangeClass">
<div class="d-content" v-if="allowedToChangeClass">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -71,7 +71,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -55,7 +55,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -77,7 +77,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -94,7 +94,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -78,7 +78,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -83,7 +83,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr>
<td class="settings-label">
{{ $t("showHeader") }}
@@ -26,7 +26,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -67,7 +67,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key"
@@ -39,7 +39,7 @@
</a>
</td>
</tr>
</fragment>
</div>
</template>
<script>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -66,7 +66,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -111,7 +111,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -56,7 +56,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -60,7 +60,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -54,7 +54,7 @@
</div>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -48,7 +48,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template>
<fragment>
<div class="d-content">
<tr
v-if="!mixinData.inlineSettingMixin.modalVisible"
>
@@ -76,7 +76,7 @@
/>
</td>
</tr>
</fragment>
</div>
</template>
<style lang="scss" scoped>
+33 -33
View File
@@ -11,56 +11,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
// NOTE: when adding a page make sure to implement the `common:setTitle` action
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
const Logout = () => import('@/components/auth/logout');
// Hall
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
const HallPage = () => import('@/components/hall/index');
const PatronsPage = () => import('@/components/hall/patrons');
const HeroesPage = () => import('@/components/hall/heroes');
// Admin Pages
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
const AdminContainerPage = () => import('@/components/admin/container');
const AdminPanelPage = () => import('@/components/admin/admin-panel');
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
const GroupAdminPage = () => import('@/components/admin/groups');
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
const BlockerPage = () => import('@/components/admin/blocker');
// Tasks
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
const UserTasks = () => import('@/components/tasks/user');
// Inventory
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
const InventoryContainer = () => import('@/components/inventory/index');
const ItemsPage = () => import('@/components/inventory/items/index');
const EquipmentPage = () => import('@/components/inventory/equipment/index');
const StablePage = () => import('@/components/inventory/stable/index');
// Guilds & Parties
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
const GroupPage = () => import('@/components/groups/group');
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
const LookingForParty = () => import('@/components/groups/lookingForParty');
// Group Plans
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
const GroupPlanIndex = () => import('@/components/group-plans/index');
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import('@/components/group-plans/billing');
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
// Challenges
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
const ChallengeIndex = () => import('@/components/challenges/index');
const MyChallenges = () => import('@/components/challenges/myChallenges');
const FindChallenges = () => import('@/components/challenges/findChallenges');
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
// Shops
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
const ShopsContainer = () => import('@/components/shops/index');
const MarketPage = () => import('@/components/shops/market/index');
const QuestsPage = () => import('@/components/shops/quests/index');
const CustomizationsPage = () => import('@/components/shops/customizations/index');
const SeasonalPage = () => import('@/components/shops/seasonal/index');
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
Vue.use(VueRouter);
+2 -2
View File
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
const OverviewPage = () => import('@/components/static/overview');
const PressKitPage = () => import('@/components/static/pressKit');
const PrivacyPage = () => import('@/components/static/privacy');
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
const RegisterUsername = () => import('@/components/auth/registerUsername');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const TermsPage = () => import('@/components/static/terms');