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
1 change: 0 additions & 1 deletion frontend/src/pages/Enhance.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,6 @@ for (const part of parts) {
} finally {
setEnhancing(false)
}
}
}

const handleGeneratePortfolio = async () => {
Expand Down
73 changes: 66 additions & 7 deletions frontend/src/pages/JobAlerts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
import toast from 'react-hot-toast';
import { jobAlertsApi, jobsApi } from '../services/api';
import { JobAlertModal, JobAlertsList } from '../components';
import { SkeletonStatCards, SkeletonJobList } from '../components/ui/Skeleton'

import { SkeletonStatCards, SkeletonJobList } from '../components/ui/Skeleton';
import { isValidEmail } from '../utils/email';
export default function JobAlerts() {
const [activeTab, setActiveTab] = useState('alerts'); // 'alerts' | 'search'
const [isModalOpen, setIsModalOpen] = useState(false);
Expand Down Expand Up @@ -280,11 +280,31 @@ function JobCard({ job, index }) {
}
};

const handleEmail = () => {
if (job.recruiterEmail) {
window.location.href = `mailto:${job.recruiterEmail}?subject=Application for ${job.title}`;
}
};
const [showMailtoConfirm, setShowMailtoConfirm] = useState(false);
const recruiterEmailIsValid = isValidEmail(job.recruiterEmail);

const handleEmail = () => {
handleMailtoFallback();
};

const handleMailtoFallback = () => {
if (!recruiterEmailIsValid) {
toast.error("That recruiter email doesn't look valid, so we can't open your mail client.");
return;
}
setShowMailtoConfirm(true);
};

const confirmMailtoFallback = () => {
setShowMailtoConfirm(false);
if (!isValidEmail(job.recruiterEmail)) {
toast.error("That recruiter email doesn't look valid, so we can't open your mail client.");
return;
}
const safeEmail = job.recruiterEmail.trim();
const safeSubject = encodeURIComponent(`Application for ${job.title || 'this role'}`);
window.location.href = `mailto:${safeEmail}?subject=${safeSubject}`;
Comment on lines +304 to +306

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Email is not URL-encoded before being placed in the mailto: URL, allowing parameter injection.

The subject is escaped, but safeEmail is interpolated raw. EMAIL_REGEX permits mailto-significant characters in the local part (?, &, =), so a value like foo?cc=victim@example.com passes validation yet injects extra mailto headers/parameters — exactly the query-parameter injection this PR set out to prevent. Encode the address (and split off any local-part edge cases) before building the URL.

🛡️ Encode the address path
-  const safeEmail = job.recruiterEmail.trim();
+  const safeEmail = encodeURIComponent(job.recruiterEmail.trim());
   const safeSubject = encodeURIComponent(`Application for ${job.title || 'this role'}`);
   window.location.href = `mailto:${safeEmail}?subject=${safeSubject}`;

Note: encodeURIComponent will percent-encode @; if that breaks your target mail clients, encode only the local part or tighten EMAIL_REGEX to exclude ?, &, and =.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const safeEmail = job.recruiterEmail.trim();
const safeSubject = encodeURIComponent(`Application for ${job.title || 'this role'}`);
window.location.href = `mailto:${safeEmail}?subject=${safeSubject}`;
const safeEmail = encodeURIComponent(job.recruiterEmail.trim());
const safeSubject = encodeURIComponent(`Application for ${job.title || 'this role'}`);
window.location.href = `mailto:${safeEmail}?subject=${safeSubject}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/JobAlerts.jsx` around lines 310 - 312, The mailto URL in
JobAlerts.jsx is still vulnerable because safeEmail is inserted raw while only
the subject is encoded. Update the email-link construction around the
job.recruiterEmail handling to URL-encode the address portion before
concatenating it into the mailto: string, and consider tightening EMAIL_REGEX or
encoding just the local part if full encodeURIComponent breaks mail clients.
Keep the existing subject encoding and build the final URL only from
encoded/validated pieces in the same flow.

};

return (
<motion.div
Expand Down Expand Up @@ -354,8 +374,47 @@ function JobCard({ job, index }) {
: job.description}
</p>
)}

{recruiterEmailIsValid && (
<button
type="button"
onClick={handleMailtoFallback}
className="mt-2 text-xs text-muted-foreground underline hover:text-foreground cursor-pointer"
>
Prefer your own mail client? Email recruiter directly
</button>
)}
</div>
</div>

{showMailtoConfirm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 px-4">
<div className="bg-card border border-border rounded-xl p-6 max-w-sm w-full">
<h4 className="font-semibold text-foreground text-base mb-2">
Open your mail client to apply?
</h4>
<p className="text-sm text-muted-foreground mb-5">
This will leave Career Pilot and open your default email app with the recruiter's address pre-filled.
</p>
<div className="flex justify-end gap-2">
<button
type="button"
onClick={() => setShowMailtoConfirm(false)}
className="px-4 py-2 rounded-lg text-sm font-medium text-foreground hover:bg-muted/40 cursor-pointer"
>
Cancel
</button>
<button
type="button"
onClick={confirmMailtoFallback}
className="px-4 py-2 rounded-lg text-sm font-medium bg-primary text-primary-foreground hover:opacity-90 cursor-pointer"
>
Open mail client
</button>
</div>
</div>
</div>
)}
</motion.div>
);
}
15 changes: 15 additions & 0 deletions frontend/src/utils/email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const MAX_EMAIL_LENGTH = 254;
const EMAIL_REGEX =
/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;

export function isValidEmail(email) {
if (typeof email !== 'string') return false;

const trimmed = email.trim();

if (trimmed.length === 0 || trimmed.length > MAX_EMAIL_LENGTH) {
return false;
}

return EMAIL_REGEX.test(trimmed);
}
Loading