diff --git a/docs/blog/2025-06-12-cve-2025-36852-critical-cache-poisoning-vulnerability-creep.md b/docs/blog/2025-06-12-cve-2025-36852-critical-cache-poisoning-vulnerability-creep.md new file mode 100644 index 0000000000..2e06e995d3 --- /dev/null +++ b/docs/blog/2025-06-12-cve-2025-36852-critical-cache-poisoning-vulnerability-creep.md @@ -0,0 +1,96 @@ +--- +title: 'CVE-2025-36852: Critical Cache Poisoning Vulnerability Affects Multiple Build Systems' +slug: cve-2025-36852-critical-cache-poisoning-vulnerability-creep +authors: ['Victor Savkin'] +tags: ['caching', 'security'] +pinned: false +description: 'A critical security vulnerability called CREEP (Cache Race-condition Exploit Enables Poisoning) has been published as CVE-2025-36852. This vulnerability affects remote cache plugins across numerous build systems, including Nx.' +--- + +A critical security vulnerability called **CREEP** (Cache Race-condition Exploit Enables Poisoning) has been published as **CVE-2025-36852**. This vulnerability affects remote cache plugins across numerous build systems, including Nx. + +The CREEP vulnerability allows any contributor with pull request privileges to inject compromised artifacts into production environments without detection. While it primarily impacts bucket-based caching solutions (such as S3, GCS, or similar object storage), it can also affect other build systems with similar caching architectures. + +**Key Points:** + +- Nx without remote cache is **NOT** affected +- Nx Cloud is **NOT** affected due to its security architecture +- Review this post to determine if your self-hosted cache solution is vulnerable + +## **Understanding the Vulnerability** + +A typical remote-cache flow using storage services follows these steps: + +1. Artifact construction (via bundler, compiler, etc.) +2. Artifact packaging (by Nx or similar tool) +3. Encryption and hashing of the packaged artifact +4. Uploading the encrypted artifact to storage (transit) +5. Storing artifacts until needed (at rest) +6. Downloading from storage (transit) +7. Decryption of the packaged artifact +8. Unpacking + +Traditional cache poisoning attacks occur during transit or storage. The CREEP vulnerability is fundamentally different—it exploits the artifact construction phase itself, before any transit or storage security measures take effect. Because poisoning happens during construction, malicious data is sent and inserted into the cache through the system's own protected mechanisms. + +The security threat comes from creating a branch with the same file system state and simplified CI setup, such that it can execute the build before the main branch and upload the artifacts. It's caused by a race condition where the "first-to-cache-wins" principle applies. Whichever branch or PR first uploads a build artifact for a particular source file state will have its version used everywhere that source state appears, including production deployments. + +**Critical implications:** + +- Correct and poisoned artifacts are identical from a validation perspective +- Checksums always match because poisoning occurs before hashing +- No direct remote cache access is required to execute the attack +- Standard detection methods cannot identify compromised artifacts +- Traditional security protections (encryption, access control, key management) do not address this vulnerability + +## **Severity** + +CVE-2025-36852 has a severity score of 9.4 (Critical). It requires only low privileges and enables attackers to perform: + +- Code execution +- Data exfiltration +- Lateral movement +- Additional attack vectors + +## **How Nx Cloud Prevents This Attack** + +Nx Cloud's architecture inherently prevents this vulnerability through: + +### **1\. Hierarchical Caching System** + +Nx Cloud implements a two-tier caching hierarchy: + +- Protected branches (like main) can write to the shared cache +- Feature branches can only write to their own isolated, branch-scoped caches +- All branches can read from the shared cache, but write privileges are strictly controlled + +### **2\. VCS Integration** + +Nx Cloud integrates directly with version control systems to enforce proper cache scoping, ensuring cache permissions align with code permissions. + +### **3\. Trust Boundaries** + +By establishing clear trust boundaries between different branch types, Nx Cloud eliminates the race condition that makes CVE-2025-36852 possible. + +## **What Organizations Should Do** + +### **For Nx Cloud users:** + +- No action required +- Continue following security best practices for your CI/CD pipeline + +### **For systems using self-hosted cache:** + +1. Read and understand [CVE-2025-36852](https://www.cve.org/CVERecord?id=CVE-2025-36852) +2. Review the detailed analysis at [https://nx.app/files/cve-2025-06](https://nx.app/files/cve-2025-06) +3. Assess your exposure—any system where PRs and main branches share the same cache is vulnerable + +## **Conclusion** + +CVE-2025-36852 represents a serious threat to organizations using vulnerable caching systems. The "first-to-cache-wins" principle many build systems rely on creates an exploitable race condition that traditional security measures cannot address. + +**Action Required:** + +- If your organization uses bucket-based remote caching: immediate action is required +- If your organization uses other self-hosted remote cache solutions: immediate review required (most self-hosted caching solutions across many build systems—not just JavaScript, but also Java—are affected) +- If using Nx without remote caching: no action is required +- If using Nx with Nx Cloud: [Review your settings](/ci/concepts/cache-security#use-scoped-tokens-in-ci). If you are using default settings, no actions should be required. diff --git a/nx-dev/data-access-documents/src/lib/blog.api.ts b/nx-dev/data-access-documents/src/lib/blog.api.ts index 029517f16e..ddb30f1837 100644 --- a/nx-dev/data-access-documents/src/lib/blog.api.ts +++ b/nx-dev/data-access-documents/src/lib/blog.api.ts @@ -66,7 +66,8 @@ export class BlogApi { : null, tags: frontmatter.tags ?? [], reposts: frontmatter.reposts ?? [], - pinned: frontmatter.pinned ?? false, + // Do not default to 'false' so you can 'unpin' a blog post + pinned: frontmatter.pinned ?? null, ogImage: image, ogImageType: type, filePath, diff --git a/nx-dev/ui-blog/src/lib/blog-container.tsx b/nx-dev/ui-blog/src/lib/blog-container.tsx index 1eaf4dab22..6d1cd31f11 100644 --- a/nx-dev/ui-blog/src/lib/blog-container.tsx +++ b/nx-dev/ui-blog/src/lib/blog-container.tsx @@ -25,14 +25,19 @@ export interface BlogContainerProps { tags: string[]; } -// first five blog posts contain potentially pinned plus the last published ones. They -// should be sorted by date (not just all pinned first) +// first five blog posts should prioritize pinned posts, then show recent posts +// excluding any posts that have specific slugs we want to deprioritize export function sortFirstFivePosts( posts: BlogPostDataEntry[] ): BlogPostDataEntry[] { - return posts - .slice(0, 5) - .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + // Separate posts: pinned-able posts first + const allowedPinnedPosts = posts.filter((p) => p.pinned !== false); + + allowedPinnedPosts.sort( + (a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf() + ); + + return allowedPinnedPosts.slice(0, 5); } export function BlogContainer({ blogPosts, tags }: BlogContainerProps) { @@ -66,8 +71,15 @@ export function BlogContainer({ blogPosts, tags }: BlogContainerProps) { ); function updateBlogPosts() { - setFirstFiveBlogs(sortFirstFivePosts(filteredList)); - setRemainingBlogs(filteredList.length > 5 ? filteredList.slice(5) : []); + const firstFive = sortFirstFivePosts(filteredList); + setFirstFiveBlogs(firstFive); + + // Get the remaining blogs, sorted by date (unpinned posts after the first 5) + const firstFiveSlugs = new Set(firstFive.map((post) => post.slug)); + const remaining = filteredList + .filter((post) => !firstFiveSlugs.has(post.slug)) + .sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf()); + setRemainingBlogs(remaining); } useEffect(() => updateBlogPosts(), [filteredList]); @@ -130,9 +142,15 @@ function initializeFilters( ) { const filterBy = searchParams.get('filterBy'); + const firstFive = sortFirstFivePosts(blogPosts); + const firstFiveSlugs = new Set(firstFive.map((post) => post.slug)); + const remaining = blogPosts + .filter((post) => !firstFiveSlugs.has(post.slug)) + .sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf()); + const defaultState = { - initialFirstFive: sortFirstFivePosts(blogPosts), - initialRest: blogPosts.slice(5), + initialFirstFive: firstFive, + initialRest: remaining, initialSelectedFilterHeading: 'All Blogs', initialSelectedFilter: 'All', }; @@ -145,9 +163,17 @@ function initializeFilters( const initialFilter = ALL_TOPICS.find((filter) => filter.value === filterBy); + const filteredFirstFive = sortFirstFivePosts(result); + const filteredFirstFiveSlugs = new Set( + filteredFirstFive.map((post) => post.slug) + ); + const filteredRemaining = result + .filter((post) => !filteredFirstFiveSlugs.has(post.slug)) + .sort((a, b) => new Date(b.date).valueOf() - new Date(a.date).valueOf()); + return { - initialFirstFive: sortFirstFivePosts(result), - initialRest: result.length > 5 ? result.slice(5) : [], + initialFirstFive: filteredFirstFive, + initialRest: filteredRemaining, initialSelectedFilterHeading: initialFilter?.heading || 'All Blogs', initialSelectedFilter: initialFilter?.value || 'All', };