Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions docs/.vitepress/theme/EndevSponsors.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<section
v-if="sponsors.length"
aria-labelledby="endev-sponsors-title"
class="EndevSponsors"
>
<div class="EndevSponsorsInner">
<p id="endev-sponsors-title" class="EndevSponsorsTitle">
Company sponsors
</p>
<div class="EndevSponsorsLogos">
<a
v-for="sponsor in sponsors"
:key="sponsor.name"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 sponsor.name is not a reliable v-for key

Names are not guaranteed to be unique across sponsors, and Vue will warn (and may misreconcile the virtual DOM) on duplicate keys. Since sponsor.url is already validated as present by the filter and is inherently unique per sponsor, prefer using it as the key instead of sponsor.name.

Fix in Claude Code

:aria-label="sponsor.name"
class="EndevSponsorsLogo"
:href="sponsor.url"
rel="noopener noreferrer"
target="_blank"
>
<img :alt="sponsor.name" :src="sponsor.logo" />
</a>
</div>
<a class="EndevSponsorsCta" href="https://en.dev/#contact">
Sponsor the work
</a>
</div>
</section>
</template>

<script setup>
import { onMounted, ref } from "vue";

const sponsors = ref([]);

onMounted(async () => {
try {
const res = await fetch("https://en.dev/sponsors.json", {
headers: { Accept: "application/json" },
});
if (!res.ok) return;

const payload = await res.json();
sponsors.value = (Array.isArray(payload.sponsors) ? payload.sponsors : [])
.filter((sponsor) =>
sponsor?.kind !== "infrastructure" &&
sponsor?.name &&
sponsor?.url &&
sponsor?.logo
);
Comment on lines +45 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security Unvalidated javascript: URL from external feed

sponsor.url is fetched from https://en.dev/sponsors.json and bound directly to :href. Vue does not strip the javascript: protocol from anchor bindings, so a compromised or manipulated sponsors.json payload could deliver a javascript: URL that executes arbitrary code when a visitor clicks a sponsor logo. Adding a protocol check to the existing filter is a minimal fix.

Fix in Claude Code

} catch {
sponsors.value = [];
}
});
</script>

<style scoped>
.EndevSponsors {
border-top: 1px solid var(--vp-c-divider);
padding: 22px 24px;
}

.EndevSponsorsInner {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px 18px;
justify-content: center;
margin: 0 auto;
max-width: 960px;
}

.EndevSponsorsTitle {
color: var(--vp-c-text-2);
font-size: 13px;
font-weight: 600;
margin: 0;
text-transform: uppercase;
}

.EndevSponsorsLogos {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}

.EndevSponsorsLogo {
align-items: center;
border: 1px solid var(--vp-c-divider);
border-radius: 8px;
display: inline-flex;
height: 40px;
justify-content: center;
padding: 8px 12px;
transition: border-color 0.2s ease, background-color 0.2s ease;
}

.EndevSponsorsLogo:hover {
background: var(--vp-c-bg-soft);
border-color: var(--vp-c-brand-1);
}

.EndevSponsorsLogo img {
display: block;
max-height: 22px;
max-width: 120px;
}

.EndevSponsorsCta {
color: var(--vp-c-text-2);
font-size: 13px;
font-weight: 500;
text-decoration: none;
transition: color 0.2s ease;
}

.EndevSponsorsCta:hover {
color: var(--vp-c-brand-1);
}
</style>
3 changes: 2 additions & 1 deletion docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { h } from "vue";
import AubeSocialLinks from "./AubeSocialLinks.vue";
import BenchChart from "./BenchChart.vue";
import EndevFooter from "./EndevFooter.vue";
import EndevSponsors from "./EndevSponsors.vue";
import HomeLanding from "./HomeLanding.vue";
import { initBanner } from "./banner";
import "./custom.css";
Expand All @@ -12,7 +13,7 @@ export default {
extends: DefaultTheme,
Layout() {
return h(DefaultTheme.Layout, null, {
"layout-bottom": () => h(EndevFooter),
"layout-bottom": () => [h(EndevSponsors), h(EndevFooter)],
"nav-bar-content-after": () => h(AubeSocialLinks),
"nav-screen-content-after": () => h(AubeSocialLinks),
});
Expand Down
Loading