Skip to content
Open
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
40 changes: 20 additions & 20 deletions templates/project.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
<!-- Page title uses the project title injected by Flask's Jinja2 template engine -->

<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<title>{{ project.title }} — {{ project.level }}{% if project.skills %} {{ project.skills[0] }}{% endif %} Project | DevPath</title>
<title>{{ project.title|e }} — {{ project.level|e }}{% if project.skills %} {{ project.skills[0]|e }}{% endif %} Project | DevPath</title>

<meta property="og:title" content="{{ project.title }} — DevPath" />
<meta property="og:description" content="{{ project.description[:155] }}" />
<meta property="og:title" content="{{ project.title[:100]|e }} — DevPath" />
<meta property="og:description" content="{{ project.description[:155]|e }}" />
<meta property="og:type" content="article" />
<meta property="og:url" content="https://mydevpath-github.vercel.app/project/{{ project.id }}" />

<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="{{ project.title }} — DevPath" />
<meta name="twitter:description" content="{{ project.description[:155] }}" />
<meta name="twitter:title" content="{{ project.title[:100]|e }} — DevPath" />
<meta name="twitter:description" content="{{ project.description[:155]|e }}" />
<link rel="stylesheet" href="/static/style.css" />
<link
href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
Expand Down Expand Up @@ -44,25 +44,25 @@
<div class="detail-hero">
<div class="container">
<!-- Breadcrumb trail -->
<nav class="breadcrumb" aria-label="breadcrumb">
<nav class="breadcrumb" aria-label="breadcrumb">
<a href="/">Home</a>
<span class="breadcrumb-sep">&#8250;</span>
<span>{{ project.title }}</span>
<span>{{ project.title|e }}</span>
</nav>

<div class="detail-hero-content">
<div class="detail-hero-left">
<h1 class="detail-title">{{ project.title }}</h1>
<h1 class="detail-title">{{ project.title|e }}</h1>
<!-- Difficulty, interest, and skill badges -->
<div class="badge-group">
<span class="badge badge--{{ project.level | lower }}">{{ project.level }}</span>
<span class="badge badge--{{ project.interest | lower }}">{{ project.interest }}</span>
<span class="badge badge--{{ project.level | lower | replace(' ', '-') }}">{{ project.level|e }}</span>
<span class="badge badge--{{ project.interest | lower | replace(' ', '-') }}">{{ project.interest|e }}</span>
{% for skill in project.skills %}
<span class="badge badge--{{ skill | lower }}">{{ skill }}</span>
<span class="badge badge--{{ skill | lower | replace(' ', '-') }}">{{ skill|e }}</span>
{% endfor %}
<span class="badge badge--time">{{ project.time }} effort</span>
<span class="badge badge--time">{{ project.time|e }} effort</span>
</div>
<p class="detail-description">{{ project.description }}</p>
<p class="detail-description">{{ project.description|e }}</p>
</div>

<!-- Quick-action buttons in the hero -->
Expand Down Expand Up @@ -121,7 +121,7 @@ <h2>Features</h2>
<polyline points="20 6 9 17 4 12" />
</svg>
</span>
{{ feature }}
{{ feature|e }}
</li>
{% endfor %}
</ul>
Expand Down Expand Up @@ -152,7 +152,7 @@ <h2>Project Roadmap</h2>
<!-- Step number badge derived from loop index -->
<span class="roadmap-step-num">Step {{ loop.index }}</span>
<!-- Strip the "Step N:" prefix if present in the data -->
<p class="roadmap-step-text">{{ step | replace("Step " + loop.index|string + ": ", "") }}</p>
<p class="roadmap-step-text">{{ (step | replace("Step " + loop.index|string + ": ", ""))|e }}</p>
</div>
</li>
{% endfor %}
Expand All @@ -179,7 +179,7 @@ <h2>Learning Resources</h2>
{% set parts = resource.split(": http") %}
{% if parts|length > 1 %}
<a href="http{{ parts[1] }}" target="_blank" rel="noopener noreferrer" class="resource-link">
<span>{{ parts[0] }}</span>
<span>{{ parts[0]|e }}</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
Expand All @@ -188,7 +188,7 @@ <h2>Learning Resources</h2>
</svg>
</a>
{% else %}
<span class="resource-plain">{{ resource }}</span>
<span class="resource-plain">{{ resource|e }}</span>
{% endif %}
</li>
{% endfor %}
Expand All @@ -213,7 +213,7 @@ <h3 class="sidebar-card-title">
</h3>
<div class="tech-tags">
{% for tech in project.tech_stack %}
<span class="tech-tag">{{ tech }}</span>
<span class="tech-tag">{{ tech|e }}</span>
{% endfor %}
</div>
</div>
Expand Down Expand Up @@ -337,8 +337,8 @@ <h3 class="sidebar-card-title">
<button id="scroll-top-btn" aria-label="Back to top" title="Back to top">↑</button>

<!-- Pass project ID to the JavaScript without hardcoding -->
<script>
var PROJECT_ID = parseInt("{{ project.id }}", 10);
<script>
var PROJECT_ID = {{ project.id|tojson }};
</script>
<script src="/static/script.js"></script>
</body>
Expand Down
20 changes: 20 additions & 0 deletions tests/test_index_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from app import app
from flask import render_template


def test_index_template_escapes_malicious_stats():
# Inject malicious strings into stats to simulate unsafe data
malicious_stats = {
"total_projects": '<script>alert(1)</script>',
"unique_skills": '<img src=x onerror=alert(2)>',
"beginner_friendly": '<svg/onload=alert(3)>'
}

with app.app_context():
rendered = render_template('index.html', stats=malicious_stats)

# Ensure injected scripts and raw tags are escaped
assert '<script>alert(1)</script>' not in rendered
assert '<img src=x onerror=alert(2)>' not in rendered
assert '<svg/onload=alert(3)>' not in rendered
assert '&lt;script' in rendered or '&lt;img' in rendered
34 changes: 34 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from app import app
from flask import render_template


def test_project_template_escapes_malicious_content():
malicious_project = {
"id": 999,
"title": '<script>alert("XSS")</script>',
"level": 'Beginner',
"interest": 'Web',
"skills": ['Python'],
"description": '<img src=x onerror=alert(1)>',
"features": ['<svg/onload=alert(2)>'],
"roadmap": ['Step 1: <script>bad()</script>'],
"resources": ['Malicious: http://example.com', '<script>alert(3)</script>'],
"tech_stack": ['<b>bash</b>']
}

with app.app_context():
rendered = render_template('project.html', project=malicious_project)

# Ensure user-supplied script payloads do not appear unescaped
assert '<script>alert("XSS")</script>' not in rendered
assert '<script>bad()</script>' not in rendered
assert '<script>alert(3)</script>' not in rendered

# The raw tag text should be escaped where expected (no live HTML tags)
assert '<img' not in rendered
# Legit SVG icons exist in template; ensure no inline event-injection like '<svg/onload' is present
assert '<svg/onload' not in rendered
assert '&lt;img' in rendered or '&lt;script' in rendered

# PROJECT_ID should be safely serialized as a number in JS
assert 'var PROJECT_ID = 999' in rendered
Loading