Skip to content
Merged
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
178 changes: 178 additions & 0 deletions .claude/security-guidelines.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# 🔒 AI-Pulse Security Guidelines for Claude Code

## Purpose
These guidelines prevent common security vulnerabilities from being introduced in AI-Pulse. Follow these rules when writing code.

---

## 1. INPUT SANITIZATION & XSS PREVENTION

### ❌ DO NOT:
- Use simple regex `.replace(/<[^>]*>/g, '')` to sanitize HTML
- Directly insert user input into `innerHTML`, `eval()`, or script content
- Trust URL query parameters without validation
- Use `Math.random()` for security-sensitive values

### ✅ DO:
- **Always use DOMPurify** for HTML sanitization:
```js
const clean = DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] });
```
- **Escape HTML entities** after DOMPurify:
```js
function htmlEscape(input) {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
```
- **Validate URLs before iframe.src**:
```js
function isValidHttpUrl(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (_) {
return false;
}
}
```

---

## 2. CRYPTOGRAPHIC OPERATIONS

### ❌ DO NOT:
- Use `Math.random()` for session IDs, tokens, or any security value
- Generate predictable values with timestamps alone

### ✅ DO:
- Use `crypto.getRandomValues()` for random values:
```js
const randomBytes = new Uint8Array(6);
crypto.getRandomValues(randomBytes);
const randomHex = Array.from(randomBytes)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
```

---

## 3. SECRET MANAGEMENT

### ❌ DO NOT:
- Hardcode emails, API keys, or credentials in code
- Expose personal data in README or comments
- Commit `.env` files or private keys

### ✅ DO:
- **Use GitHub Secrets** for:
- `GIT_AUTHOR_EMAIL` → phoenix.project@outlook.fr
- `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`
- `LINKEDIN_ACCESS_TOKEN`, `LINKEDIN_USER_ID`
- `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`
- **Check .gitignore** excludes:
- `.env*` (except .env.example)
- `*.key`, `*.pem`
- `**/credentials/`, `**/secrets/`

---

## 4. DEPENDENCY VERSIONS

### ❌ DO NOT:
- Use unversioned dependencies (`latest`)
- Ignore security warnings from npm audit

### ✅ DO:
- Pin versions in package.json
- Use Dependabot for automated updates
- Review CVE alerts from CodeQL

---

## 5. URL HANDLING

### ❌ DO NOT:
- Use user input directly in:
- `window.location`
- `iframe.src`
- `fetch()` URLs
- Redirect targets

### ✅ DO:
- Validate URLs with `isValidHttpUrl()` BEFORE use
- Use relative URLs when possible
- Explicitly assign only after validation:
```js
let safeUrl = null;
if (userUrl && isValidHttpUrl(userUrl)) {
safeUrl = userUrl;
}
iframe.src = safeUrl; // Now safe to use
```

---

## 6. CODE REVIEW CHECKLIST

Before submitting any code, verify:

- [ ] No regex-only HTML sanitization (use DOMPurify)
- [ ] No hardcoded secrets or emails
- [ ] No `Math.random()` for security values
- [ ] URLs validated before iframe/redirect
- [ ] No `innerHTML` with unsanitized input
- [ ] All user input escaped with `htmlEscape()`
- [ ] No exposed personal data in comments
- [ ] `.gitignore` includes all sensitive patterns
- [ ] Dependabot configured (2x daily updates)
- [ ] CodeQL alerts reviewed and fixed

---

## 7. KNOWN VULNERABILITIES (DO NOT REPEAT)

### Previously Fixed:
1. **Incomplete Multi-Character Sanitization** (aggregator.js:68)
- Fixed with: DOMPurify + htmlEscape

2. **Unvalidated URL Redirection** (reader.html:496)
- Fixed with: Explicit validation before iframe.src assignment

3. **Insecure Randomness** (tracker.js:8)
- Fixed with: crypto.getRandomValues()

---

## 8. TOOLS & RESOURCES

- **DOMPurify**: Sanitize HTML safely
- **crypto API**: Secure random generation
- **CodeQL**: Detect XSS, injection, randomness issues
- **GitHub Secrets**: Store sensitive values
- **Dependabot**: Automate dependency updates

---

## 9. DEPLOYMENT SECURITY

### For Vercel:
- [ ] All secrets in Vercel Environment Variables
- [ ] No .env files in repo (use .env.example)
- [ ] ALLOWED_TAGS in DOMPurify explicitly set to []
- [ ] Content-Security-Policy headers configured
- [ ] CORS properly restricted

---

## Questions?

If unsure about security implications:
1. Ask this guide first
2. Check CodeQL rules
3. Consult OWASP Top 10
4. Run `npm audit`

**Remember**: Security is not a feature, it's a requirement.
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
{
"name": "ai-pulse",
"version": "1.0.0",
"description": "Curated AI & Cybersecurity news aggregator",
"main": "src/aggregator.js",
"scripts": {
"start": "node src/aggregator.js",
"dev": "node src/aggregator.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["ai", "cybersecurity", "news", "aggregator", "rss"],
"author": "ThePhoenixAgency",
"license": "MIT",
"dependencies": {
"@octokit/rest": "^22.0.1",
"axios": "^1.13.2",
Expand All @@ -7,6 +19,10 @@
"dompurify": "^3.3.1",
"express": "^5.2.1",
"isomorphic-dompurify": "^2.35.0",
"rss-parser": "^3.13.0"
},
"engines": {
"node": ">=20.0.0"
"rss-parser": "^3.13.0",
"sanitize-html": "^2.17.0"
}
Expand Down
10 changes: 8 additions & 2 deletions reader.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src *; connect-src 'self'; img-src 'self' data: https:">

Choose a reason for hiding this comment

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

P2 Badge Allow external scripts required by reader

The new CSP restricts script-src to 'self', but reader.html still loads DOMPurify from https://cdn.jsdelivr.net/... (and gtag from https://www.googletagmanager.com). Browsers enforcing CSP will block those scripts, so when the inline script later calls DOMPurify.sanitize(...) it throws a ReferenceError and breaks the article view for typical URLs that include source/tags. Consider whitelisting the required CDN origins in script-src or self-hosting those scripts so the sanitizer is available.

Useful? React with 👍 / 👎.

Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The Content-Security-Policy allows 'unsafe-inline' for both script-src and style-src, which significantly weakens the CSP protection against XSS attacks. Consider using nonces or hashes for inline scripts/styles, or move inline code to external files. Additionally, 'frame-src *' allows iframes from any source which may be too permissive.

Copilot uses AI. Check for mistakes.
<title>AI-Pulse Reader</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
Expand Down Expand Up @@ -440,7 +441,7 @@ <h2>Impossible de charger l'article</h2>

// Parse URL Parameters
const params = new URLSearchParams(window.location.search);
const articleUrl = params.get('url');
const rawArticleUrl = params.get('url');
const articleTitle = params.get('title');
const articleSource = params.get('source');
const articleTags = params.get('tags');
Expand All @@ -456,6 +457,12 @@ <h2>Impossible de charger l'article</h2>
}
}

// Validate and sanitize URL - only proceed if URL is safe
let articleUrl = null;
if (rawArticleUrl && isValidHttpUrl(rawArticleUrl)) {
articleUrl = rawArticleUrl; // Now articleUrl is guaranteed to be a valid http(s) URL
}

// If article parameters exist and URL is valid, show article view
if (articleUrl && isValidHttpUrl(articleUrl)) {
const safeArticleUrl = new URL(articleUrl).toString();
Expand All @@ -472,7 +479,6 @@ <h2>Impossible de charger l'article</h2>
} catch (_) {
return false;
}
}

// If article parameters exist and URL is valid and allowed, show article view
if (articleUrl && isAllowedArticleUrl(articleUrl)) {
Expand Down
46 changes: 31 additions & 15 deletions src/aggregator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,27 @@ const FEED_CATEGORIES = {
]
};

// UTM parameters for AI-Pulse traffic tracking
// Tracks clicks sent FROM AI-Pulse TO external sites
function addUTMParams(url, category = 'general') {
const utmParams = `utm_source=ai-pulse&utm_medium=reader&utm_campaign=article&utm_content=${category}`;
return url.includes('?') ? `${url}&${utmParams}` : `${url}?${utmParams}`;
// Article links for external sources
function getArticleLink(url, category = 'general') {
// Return direct link without any tracking parameters
// Users can add their own UTM if needed
return url;
}

Comment on lines +25 to +31
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The function getArticleLink is defined but never used in the code. Since it only returns the URL unchanged, consider removing this unused function to improve code maintainability.

Suggested change
// Article links for external sources
function getArticleLink(url, category = 'general') {
// Return direct link without any tracking parameters
// Users can add their own UTM if needed
return url;
}

Copilot uses AI. Check for mistakes.
/**
* HTML-escape a string so it is safe to insert into HTML contexts.
* Converts &, <, and > to their corresponding entities.
* @param {string} input
* @returns {string}
*/
function htmlEscape(input) {
if (!input) {
return '';
}
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
Comment on lines +38 to +45
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The htmlEscape function is incomplete. It only escapes &, <, and > characters but doesn't handle other important HTML entities like quotes (" and '). This could lead to XSS vulnerabilities in attribute contexts. Consider adding escaping for quotes: .replace(/"/g, '"') and .replace(/'/g, ''')

Copilot uses AI. Check for mistakes.
}

// Robust HTML sanitization: strip all tags and unsafe content
Expand Down Expand Up @@ -108,7 +124,7 @@ function sanitizeArticle(article, sourceName, tags, category) {
source: sourceName,
tags: tags,
category: article.categories?.[0] || 'General',
summary: smartTruncate(rawSummary, 600) // Increased to 600 with smart truncation for better article previews
summary: smartTruncate(htmlEscape(cleanSummary), 600) // DOMPurify removes tags, htmlEscape handles entities
};
}

Expand Down Expand Up @@ -169,9 +185,9 @@ function generateREADME(categorizedArticles) {

**Built by [ThePhoenixAgency](https://github.com/ThePhoenixAgency)** - AI & Cybersecurity Specialist

🔥 **[View My Portfolio](https://thephoenixagency.github.io/AI-Pulse/portfolio.html)** |
📊 **[Live Stats Dashboard](https://thephoenixagency.github.io/AI-Pulse/stats.html)** |
🚀 **[Launch Reader App](https://thephoenixagency.github.io/AI-Pulse/reader.html)**
🔥 **[GitHub Repository](https://github.com/ThePhoenixAgency/AI-Pulse)** |
📊 **[Organization](https://github.com/ThePhoenixAgency)** |
🚀 **[Follow Us](https://github.com/ThePhoenixAgency)**

> Passionate about building secure, privacy-first applications that make a difference.
> This project showcases my expertise in full-stack development, security engineering, and data privacy.
Expand Down Expand Up @@ -224,14 +240,14 @@ function generateREADME(categorizedArticles) {
readme += `## 🧭 Navigation\n\n`;
readme += `<div align="center">\n\n`;
readme += `### Explore AI-Pulse\n\n`;
readme += `| 🏠 [Main App](https://thephoenixagency.github.io/AI-Pulse/reader.html) | 👨‍💻 [Portfolio](https://thephoenixagency.github.io/AI-Pulse/portfolio.html) | 📊 [Stats](https://thephoenixagency.github.io/AI-Pulse/stats.html) | 📚 [Docs](./database/SUPABASE_MIGRATION.md) |\n`;
readme += `|:---:|:---:|:---:|:---:|\n`;
readme += `| Read articles in-app | View my projects | Analytics dashboard | Migration guide |\n\n`;
readme += `| 📚 [Repository](https://github.com/ThePhoenixAgency/AI-Pulse) | 👨‍💻 [Organization](https://github.com/ThePhoenixAgency) | 🔐 [Docs](./database/SUPABASE_MIGRATION.md) |\n`;
readme += `|:---:|:---:|:---:|\n`;
readme += `| Source Code | Team Profile | Technical Docs |\n\n`;
readme += `---\n\n`;
readme += `### 🤝 Connect With Me\n\n`;
readme += `[![GitHub Profile](https://img.shields.io/badge/GitHub-EthanThePhoenix38-181717?style=for-the-badge&logo=github)](https://github.com/EthanThePhoenix38)\n`;
readme += `[![Organization](https://img.shields.io/badge/Organization-ThePhoenixAgency-181717?style=for-the-badge&logo=github)](https://github.com/ThePhoenixAgency)\n`;
readme += `[![Website](https://img.shields.io/badge/Website-ThePhoenixAgency.github.io-blue?style=for-the-badge&logo=google-chrome&logoColor=white)](https://ThePhoenixAgency.github.io)\n\n`;
readme += `[![GitHub Profile](https://img.shields.io/badge/GitHub-ThePhoenixAgency-181717?style=for-the-badge&logo=github)](https://github.com/ThePhoenixAgency)\n`;
readme += `[![Repository](https://img.shields.io/badge/Repository-AI--Pulse-181717?style=for-the-badge&logo=github)](https://github.com/ThePhoenixAgency/AI-Pulse)\n`;
readme += `[![Support](https://img.shields.io/badge/Support-Issues-181717?style=for-the-badge&logo=github)](https://github.com/ThePhoenixAgency/AI-Pulse/issues)\n\n`;
readme += `---\n\n`;
readme += `<sub>*Powered by [AI-Pulse](https://github.com/ThePhoenixAgency/AI-Pulse) | 100% Free & Open Source | Built with ❤️ by ThePhoenixAgency*</sub>\n\n`;
readme += `</div>\n`;
Expand Down
3 changes: 2 additions & 1 deletion tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class AIPlulseTracker {
}

/**
* Generate a unique session ID (not personally identifiable)
* Generate a unique session ID using cryptographically secure randomness
* (not personally identifiable)
*/
generateSessionId() {
// Use cryptographically secure random values instead of Math.random
Expand Down
66 changes: 66 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"version": 2,
"name": "ai-pulse",
"builds": [
{
"src": "src/aggregator.js",
"use": "@vercel/node"
},
{
"src": "*.html",
"use": "@vercel/static"
},
{
"src": "*.js",
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

Using "@vercel/static" for "*.js" files will serve all JavaScript files as static assets, which may not be the intended behavior. This could expose server-side JavaScript files that should not be publicly accessible. Consider being more specific about which JS files should be served statically (e.g., only files in a specific public directory).

Suggested change
"src": "*.js",
"src": "public/*.js",

Copilot uses AI. Check for mistakes.
"use": "@vercel/static"
}
],
"routes": [
{
"src": "/api/(.*)",
"dest": "src/aggregator.js"
},
{
"src": "/(.*)",
"dest": "/$1"
}
],
"env": {
"NODE_ENV": "production"
},
"buildCommand": "npm install",
"outputDirectory": ".",
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "SAMEORIGIN"
},
{
"key": "X-XSS-Protection",
"value": "1; mode=block"
Comment on lines +46 to +47
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The X-XSS-Protection header is deprecated and should be removed. Modern browsers have removed support for this header, and it can actually introduce security vulnerabilities in older browsers. The Content-Security-Policy header provides better XSS protection.

Suggested change
"key": "X-XSS-Protection",
"value": "1; mode=block"
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"

Copilot uses AI. Check for mistakes.
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "geolocation=(), camera=(), microphone=()"
}
]
}
],
"functions": {
"src/aggregator.js": {
"maxDuration": 300,
"memory": 1024
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The function memory allocation of 1024 MB (1 GB) seems excessive for an RSS aggregator function. Consider starting with a lower value like 512 MB or 256 MB and scaling up only if needed, as this will reduce costs and resource usage.

Suggested change
"memory": 1024
"memory": 512

Copilot uses AI. Check for mistakes.
}
}
}