Security report for now-sh/api
Hello maintainers,
I am a security researcher studying security risks in vibe-coded software. During this research, I reviewed this repository and identified the findings below. These findings were identified in commit a5c2de9. Each finding has undergone human analysis, but the report may still contain mistakes or incomplete interpretations. Please review this report and apply the necessary security fixes.
This report contains 10 confirmed findings identified during security review. Validation details below distinguish code evidence from runtime observations and note any limitations.
Summary
| ID |
Severity |
Category |
Affected area |
| SEC-001 |
High |
Authentication Failures |
api/index.js:26 |
| SEC-002 |
High |
Authentication Failures |
api/controllers/auth.js |
| SEC-003 |
High |
Injection |
api/public/js/notes.js
api/public/js/domains.js:72–74
api/public/js/todos.js
api/public/js/utility-forms-v2.js:24–27
api/public/js/timezones.js:101–103,139,144 |
| SEC-004 |
High |
Injection |
api/routes/markdownRoute.js |
| SEC-005 |
High |
Injection |
api/controllers/blog.js:189
package.json |
| SEC-006 |
High |
Injection |
api/public/js/auth.js |
| SEC-007 |
High |
Injection |
api/public/js/closings.js |
| SEC-008 |
High |
Broken Access Control |
api/routes/mainRoute.js:670–676
api/controllers/url.js:68–74 |
| SEC-009 |
Medium |
Cryptographic Failures |
api/public/js/auth.js:56,97
api/public/js/auth-cookies.js:33–34 |
| SEC-010 |
Low |
Authentication Failures |
api/controllers/auth.js |
1. Replace jwt.decode with jwt.verify to Enforce Signature Validation
ID: SEC-001
Severity: High
Category: Authentication Failures
Affected code: api/index.js:26
Impact
Requests can trust attacker-controlled JWT claims because the token signature is not validated. This can cause forged identity information to be treated as authenticated data, although the supplied runtime check did not show impact in the current version page.
Technical details
api/index.js:26 calls jwt.decode(authToken) rather than verifying the token signature.
- The decoded
username, name, or sub claim is assigned to currentUser and used to build authentication display data.
Relevant code
api/index.js:20-32
const authToken = req.cookies?.authToken || req.header('Authorization')?.replace('Bearer ', '');
let currentUser = null;
if (authToken) {
try {
const jwt = require('jsonwebtoken');
const decoded = jwt.decode(authToken);
currentUser = decoded?.username || decoded?.name || decoded?.sub || 'Authenticated User';
} catch (_error) {
// Invalid token, leave currentUser as null
}
}
Validation
Code evidence
-
api/index.js:20–32 extracts the bearer token and calls jwt.decode(authToken) without a secret or signature validation.
Runtime evidence
-
A forged alg:none token with username=admin_forged_test returned Auth: "no" from /api/v1/version.
Limitations
-
version.ejs does not render the affected versionData.Auth value; the current UI display impact was therefore not demonstrated.
Recommended remediation
Use jwt.verify() with the configured secret and enforce an allowed algorithm. Reject tokens that fail signature or claim validation before trusting their contents.
2. Set Expiry on Issued JWTs to Prevent Indefinitely Valid Tokens
ID: SEC-002
Severity: High
Category: Authentication Failures
Affected code: api/controllers/auth.js
Impact
Issued JWTs do not have a time-based expiration. A stolen token can remain usable indefinitely unless the associated database revocation record is successfully consulted and retained.
Technical details
api/controllers/auth.js:17–20 calls JWT.sign({ email }, process.env.JWT_SECRET) without an expiresIn option.
- The same token-generation function is used by signup, login, and token-rotation paths.
Relevant code
api/controllers/auth.js:1-30
const bcrypt = require('bcryptjs');
const JWT = require('jsonwebtoken');
const User = require('../models/user');
const Token = require('../models/token');
/**
* Generate JWT token (no expiration)
*/
const generateToken = (email) => {
if (!process.env.JWT_SECRET) {
throw new Error('JWT_SECRET is not configured');
}
// Sign without expiresIn option - token will never expire
return JWT.sign(
{ email },
process.env.JWT_SECRET
);
};
/**
* Save token to database
*/
const saveToken = async (token, userId, email, description) => {
await Token.create({
token,
userId,
email,
Validation
Code evidence
-
The function comment states Generate JWT token (no expiration), and the signing call omits expiration claims.
Limitations
-
The supplied live probe redirected, so this finding is based on code evidence.
Recommended remediation
Set a bounded JWT lifetime when signing tokens and retain the existing revocation check as a secondary control.
3. Escape API-Sourced Fields Before Injecting into innerHTML Across Multiple Client Scripts
ID: SEC-003
Severity: High
Category: Injection
Affected code: api/public/js/notes.js, api/public/js/domains.js:72–74, api/public/js/todos.js, api/public/js/utility-forms-v2.js:24–27, api/public/js/timezones.js:101–103,139,144
Impact
API-controlled or externally supplied values can be interpreted as HTML and potentially execute script in users' browsers when rendered through innerHTML.
Technical details
notes.js:196–213 inserts note.title, note.category, and a preview derived from note.content into container.innerHTML without escaping.
domains.js:72–74 inserts domain, and utility-forms-v2.js:26 inserts message, also without the available escapeHtml() helper.
- Sibling
todos.js and blogs.js paths use escapeHtml() for comparable values.
Relevant code
api/public/js/notes.js:1-30
let notes = [];
let currentNoteId = null;
document.addEventListener('DOMContentLoaded', function() {
// Check authentication
checkAuth();
// Event listeners
document.getElementById('newNoteBtn').addEventListener('click', createNewNote);
document.getElementById('saveNoteBtn').addEventListener('click', saveNote);
document.getElementById('deleteNoteBtn').addEventListener('click', deleteCurrentNote);
// Toolbar buttons
document.querySelectorAll('.toolbar-btn').forEach(btn => {
btn.addEventListener('click', function() {
const format = this.getAttribute('data-format');
formatText(format);
});
});
// Auto-save on content change
let saveTimer;
document.getElementById('noteContent').addEventListener('input', function() {
clearTimeout(saveTimer);
if (currentNoteId) {
saveTimer = setTimeout(() => saveNote(true), 2000);
}
});
document.getElementById('noteTitle').addEventListener('input', function() {
Validation
Code evidence
-
The affected template literals assign API-derived fields directly to innerHTML; no escaping or safe DOM API is shown.
Limitations
-
The deployed host returned HTTP 308 and live confirmation was not possible.
Recommended remediation
Escape every untrusted value before HTML interpolation, using the existing escapeHtml() helper, or replace innerHTML with textContent and safe DOM construction.
4. HTML-Encode Markdown Metadata Title to Prevent Reflected XSS
ID: SEC-004
Severity: High
Category: Injection
Affected code: api/routes/markdownRoute.js
Impact
A crafted Markdown heading can break out of the HTML <title> context in the preview response and inject script into users' browsers.
Technical details
markdownRoute.js:97 extracts the title from a raw H1 heading.
markdownRoute.js:277 interpolates metadata.title directly into <title>${metadata.title}</title> without HTML encoding.
- The body is sanitized separately, so that sanitization does not protect the title context.
Relevant code
api/routes/markdownRoute.js:1-30
const express = require('express');
const cors = require('cors');
const { marked } = require('marked');
const hljs = require('highlight.js');
const { setStandardHeaders } = require('../utils/standardHeaders');
// DOMPurify with fallback for serverless environments
let DOMPurify;
try {
DOMPurify = require('isomorphic-dompurify');
} catch {
// Fallback: basic HTML escaping if DOMPurify fails to load
DOMPurify = {
sanitize: (html, options) => {
// Allow specified tags, escape others
const allowedTags = options?.ALLOWED_TAGS || ['p', 'br', 'strong', 'em', 'code', 'pre'];
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/gi;
return html.replace(tagRegex, (match, tag) => {
return allowedTags.includes(tag.toLowerCase()) ? match : '';
});
}
};
console.warn('DOMPurify fallback active - isomorphic-dompurify not available');
}
const markdownRoute = express.Router();
// Configure marked v4 with highlight
marked.setOptions({
breaks: true,
Validation
Code evidence
-
The title value is derived from request Markdown and inserted into the response without encoding.
Runtime evidence
-
A POST to /api/v1/tools/markdown/preview with a closing-title and script payload returned the payload inside the response title context.
Recommended remediation
HTML-encode the extracted title before inserting it into the <title> element, while retaining body sanitization.
5. Sanitize Markdown-Rendered HTML in Blog Controller Before Output
ID: SEC-005
Severity: High
Category: Injection
Affected code: api/controllers/blog.js:189, package.json
Impact
GitHub-sourced Markdown can introduce executable HTML into blog pages because rendered content is emitted without sanitization.
Technical details
api/controllers/blog.js:189 assigns post.contentHtml = marked.parse(post.content) without calling DOMPurify.
api/views/pages/data/blog-post.ejs:46 emits post.contentHtml with the raw EJS operator <%- ... %>.
- The supplied code evidence identifies
[email protected] as the parser version.
Relevant code
api/controllers/blog.js:183-195
const getBlogPost = async (slug) => {
const posts = await getBlogPosts();
const post = posts.find(post => post.slug === slug);
if (post && post.content) {
// Convert markdown content to HTML
post.contentHtml = marked.parse(post.content);
}
return post;
};
/**
Validation
Code evidence
-
The blog controller renders Markdown directly, and the EJS template outputs the resulting HTML verbatim.
Runtime evidence
-
The supplied probe for /data/blogs/MCU-order confirmed blog content was rendered as unescaped HTML.
Limitations
-
The supplied evidence does not establish whether a particular live post currently contains an executable script.
Recommended remediation
Sanitize the output of marked.parse() with the existing DOMPurify dependency before assigning contentHtml, and address the reported outdated parser dependency.
6. Escape JWT Claim Values Before Setting innerHTML in Auth Page
ID: SEC-006
Severity: High
Category: Injection
Affected code: api/public/js/auth.js
Impact
HTML in JWT claims can execute when token details are displayed, allowing a forged or compromised same-origin token to trigger script in the auth page.
Technical details
api/public/js/auth.js:269–288 interpolates payload.userId and payload.email into a template assigned to tokenDetails.innerHTML.
- The assignment uses neither
textContent nor an escaping function.
Relevant code
api/public/js/auth.js:1-30
document.addEventListener('DOMContentLoaded', function() {
// Handle form toggle links
document.addEventListener('click', function(e) {
if (e.target.matches('[data-toggle-form]')) {
e.preventDefault();
const formType = e.target.getAttribute('data-toggle-form');
toggleForms(formType);
}
// Handle logout button
if (e.target.matches('[data-action="logout"]')) {
logout();
}
// Handle copy token button
if (e.target.matches('[data-action="copy-token"]')) {
copyToken();
}
// Handle edit profile button
if (e.target.matches('[data-action="edit-profile"]')) {
showEditProfile();
}
// Handle cancel edit button
if (e.target.matches('[data-action="cancel-edit"]')) {
showProfile();
}
});
Validation
Code evidence
-
JWT payload fields are inserted directly into the innerHTML template without sanitization.
Runtime evidence
-
The signup endpoint rejected an HTML-formatted email, returning an invalid-email error.
Limitations
-
Server-side email validation mitigates the registration vector, but does not protect against localStorage poisoning through another same-origin XSS.
Recommended remediation
Render JWT claim values with textContent, or HTML-escape each value before interpolation into any HTML context.
7. Escape Third-Party Closings Data Before Injecting into innerHTML
ID: SEC-007
Severity: High
Category: Injection
Affected code: api/public/js/closings.js
Impact
Malicious or compromised closings data can be rendered as HTML or script in every visitor's browser.
Technical details
api/public/js/closings.js:115–136 assigns a template literal containing closing.organization, closing.name, closing.region, closing.delay_time, and closing.reason to closingsList.innerHTML.
- No escaping is applied in this file, despite
escapeHtml() being used by sibling scripts.
Relevant code
api/public/js/closings.js:1-30
document.addEventListener('DOMContentLoaded', function() {
const closingsList = document.getElementById('closingsList');
const searchInput = document.getElementById('searchInput');
const filterButtons = document.querySelectorAll('.filter-btn');
const weatherAlert = document.getElementById('weatherAlert');
const lastUpdated = document.getElementById('lastUpdated');
let allClosings = [];
let currentFilter = 'all';
// Load closings on page load
loadClosings();
// Update every minute
setInterval(loadClosings, 60000);
// Search functionality
searchInput.addEventListener('input', (e) => {
filterAndDisplayClosings(e.target.value.toLowerCase(), currentFilter);
});
// Filter buttons
filterButtons.forEach(btn => {
btn.addEventListener('click', () => {
filterButtons.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilter = btn.getAttribute('data-filter');
filterAndDisplayClosings(searchInput.value.toLowerCase(), currentFilter);
});
});
Validation
Code evidence
-
External closings fields flow directly into an innerHTML sink without escaping.
Runtime evidence
-
GET /api/v1/world/closings returned empty arrays during the supplied probe.
Limitations
-
The empty response prevented live demonstration with a malicious closings value; the vulnerable rendering path remains reachable.
Recommended remediation
Apply escapeHtml() to every closings field before interpolation, or construct the output with safe DOM APIs and textContent.
8. Block Private and Loopback Addresses in URL Shortener to Prevent SSRF and Open Redirect
ID: SEC-008
Severity: High
Category: Broken Access Control
Affected code: api/routes/mainRoute.js:670–676, api/controllers/url.js:68–74
Impact
The unauthenticated URL shortener accepts redirects to loopback, private, and link-local destinations, enabling open redirects and potentially facilitating server-side requests to internal services by consumers or integrations.
Technical details
api/controllers/url.js:68–74 validates the destination syntactically with new URL() but does not block private, loopback, or link-local addresses.
api/routes/mainRoute.js:675 redirects directly to url.originalUrl.
- The route's
isURL() validation also permits these destinations.
Relevant code
api/controllers/url.js:1-30
const crypto = require('crypto');
const Url = require('../models/url');
const { createRepository } = require('../utils/databaseUtils');
const { getExpirationLabel, formatLastUpdated, getRelativeTimeLabel } = require('../utils/dateUtils');
const { formatCompact } = require('../utils/numberUtils');
/**
* Create URL repository
*/
const urlRepo = createRepository(Url, {
ownerField: 'owner',
publicField: 'isActive', // URLs are "public" if active
defaultSort: { createdAt: -1 },
defaultLimit: 50
});
/**
* Generate short code for URL
*/
const generateShortCode = (length = 6) => {
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
const randomBytes = crypto.randomBytes(length);
for (let i = 0; i < length; i++) {
code += charset[randomBytes[i] % charset.length];
}
return code;
};
Validation
Code evidence
-
No destination blocklist or scheme restriction is shown before the redirect.
Runtime evidence
-
A POST creating a short URL for http://169.254.169.254/latest/meta-data/ returned HTTP 200 and a short URL.
Limitations
-
The supplied evidence demonstrates acceptance and redirect behavior, not a successful metadata-service retrieval by this application.
Recommended remediation
At creation time, allow only intended schemes and reject localhost, loopback, link-local, RFC-1918, and other private or reserved address ranges. Revalidate destinations before redirecting.
9. Migrate Authentication Token Storage from localStorage to HttpOnly Cookies
ID: SEC-009
Severity: Medium
Category: Cryptographic Failures
Affected code: api/public/js/auth.js:56,97, api/public/js/auth-cookies.js:33–34
Impact
JWTs stored in JavaScript-accessible storage can be exfiltrated by any XSS executing on the origin. The cookie migration does not remove this exposure because tokens remain in localStorage and the cookie is set client-side.
Technical details
api/public/js/auth.js:56 stores login tokens with localStorage.setItem('authToken', result.data.token); the signup path at line 97 does the same.
auth-cookies.js also writes the token to localStorage and sets the cookie through document.cookie, so it is not HttpOnly.
getToken() reads from the cookie or localStorage.
Relevant code
api/public/js/auth.js:50-62
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok && result.success) {
localStorage.setItem('authToken', result.data.token);
showAuthenticatedState(result.data.token, result.data.user);
showMessage('Login successful!', 'success');
} else {
const errorMessage = result.errors && result.errors.length > 0
? result.errors.map(err => err.msg).join(', ')
: result.message || 'Login failed';
Validation
Code evidence
Recommended remediation
Move token storage to server-set cookies with HttpOnly, Secure, and SameSite=Strict attributes, then remove all localStorage token reads and writes.
10. Eliminate Timing-Based Email Enumeration in Login by Running bcrypt on Missing Accounts
ID: SEC-010
Severity: Low
Category: Authentication Failures
Affected code: api/controllers/auth.js
Impact
Login response timing can reveal whether an email corresponds to an account, enabling user enumeration through repeated measurements.
Technical details
api/controllers/auth.js:109–111 throws immediately when User.findOne({ email }) returns no user.
bcrypt.compare() at line 114 runs only for existing users, adding password-hashing work to that path.
Relevant code
api/controllers/auth.js:103-117
* Login user
*/
const login = async (email, password) => {
// Find user
const user = await User.findOne({ email });
if (!user) {
throw new Error('Invalid credentials');
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('Invalid credentials');
Validation
Code evidence
-
The missing-user branch performs no bcrypt comparison, while the existing-user branch does.
Runtime evidence
-
A nonexistent-account login returned in approximately 0.66 seconds; the supplied analysis identifies an additional approximately 100 ms bcrypt delay for an existing account with a wrong password.
Limitations
-
The runtime measurement was network-dominated, and repeated comparative sampling was not supplied.
Recommended remediation
When no user is found, perform a bcrypt comparison against a fixed dummy hash before returning the same generic authentication error.
Security report for now-sh/api
Hello maintainers,
I am a security researcher studying security risks in vibe-coded software. During this research, I reviewed this repository and identified the findings below. These findings were identified in commit
a5c2de9. Each finding has undergone human analysis, but the report may still contain mistakes or incomplete interpretations. Please review this report and apply the necessary security fixes.This report contains 10 confirmed findings identified during security review. Validation details below distinguish code evidence from runtime observations and note any limitations.
Summary
api/index.js:26api/controllers/auth.jsapi/public/js/notes.jsapi/public/js/domains.js:72–74api/public/js/todos.jsapi/public/js/utility-forms-v2.js:24–27api/public/js/timezones.js:101–103,139,144api/routes/markdownRoute.jsapi/controllers/blog.js:189package.jsonapi/public/js/auth.jsapi/public/js/closings.jsapi/routes/mainRoute.js:670–676api/controllers/url.js:68–74api/public/js/auth.js:56,97api/public/js/auth-cookies.js:33–34api/controllers/auth.js1. Replace jwt.decode with jwt.verify to Enforce Signature Validation
ID:
SEC-001Severity: High
Category: Authentication Failures
Affected code:
api/index.js:26Impact
Requests can trust attacker-controlled JWT claims because the token signature is not validated. This can cause forged identity information to be treated as authenticated data, although the supplied runtime check did not show impact in the current version page.
Technical details
api/index.js:26callsjwt.decode(authToken)rather than verifying the token signature.username,name, orsubclaim is assigned tocurrentUserand used to build authentication display data.Relevant code
api/index.js:20-32Validation
Code evidence
api/index.js:20–32extracts the bearer token and callsjwt.decode(authToken)without a secret or signature validation.Runtime evidence
A forged
alg:nonetoken withusername=admin_forged_testreturnedAuth: "no"from/api/v1/version.Limitations
version.ejsdoes not render the affectedversionData.Authvalue; the current UI display impact was therefore not demonstrated.Recommended remediation
Use
jwt.verify()with the configured secret and enforce an allowed algorithm. Reject tokens that fail signature or claim validation before trusting their contents.2. Set Expiry on Issued JWTs to Prevent Indefinitely Valid Tokens
ID:
SEC-002Severity: High
Category: Authentication Failures
Affected code:
api/controllers/auth.jsImpact
Issued JWTs do not have a time-based expiration. A stolen token can remain usable indefinitely unless the associated database revocation record is successfully consulted and retained.
Technical details
api/controllers/auth.js:17–20callsJWT.sign({ email }, process.env.JWT_SECRET)without anexpiresInoption.Relevant code
api/controllers/auth.js:1-30Validation
Code evidence
The function comment states
Generate JWT token (no expiration), and the signing call omits expiration claims.Limitations
The supplied live probe redirected, so this finding is based on code evidence.
Recommended remediation
Set a bounded JWT lifetime when signing tokens and retain the existing revocation check as a secondary control.
3. Escape API-Sourced Fields Before Injecting into innerHTML Across Multiple Client Scripts
ID:
SEC-003Severity: High
Category: Injection
Affected code:
api/public/js/notes.js,api/public/js/domains.js:72–74,api/public/js/todos.js,api/public/js/utility-forms-v2.js:24–27,api/public/js/timezones.js:101–103,139,144Impact
API-controlled or externally supplied values can be interpreted as HTML and potentially execute script in users' browsers when rendered through
innerHTML.Technical details
notes.js:196–213insertsnote.title,note.category, and a preview derived fromnote.contentintocontainer.innerHTMLwithout escaping.domains.js:72–74insertsdomain, andutility-forms-v2.js:26insertsmessage, also without the availableescapeHtml()helper.todos.jsandblogs.jspaths useescapeHtml()for comparable values.Relevant code
api/public/js/notes.js:1-30Validation
Code evidence
The affected template literals assign API-derived fields directly to
innerHTML; no escaping or safe DOM API is shown.Limitations
The deployed host returned HTTP 308 and live confirmation was not possible.
Recommended remediation
Escape every untrusted value before HTML interpolation, using the existing
escapeHtml()helper, or replaceinnerHTMLwithtextContentand safe DOM construction.4. HTML-Encode Markdown Metadata Title to Prevent Reflected XSS
ID:
SEC-004Severity: High
Category: Injection
Affected code:
api/routes/markdownRoute.jsImpact
A crafted Markdown heading can break out of the HTML
<title>context in the preview response and inject script into users' browsers.Technical details
markdownRoute.js:97extracts the title from a raw H1 heading.markdownRoute.js:277interpolatesmetadata.titledirectly into<title>${metadata.title}</title>without HTML encoding.Relevant code
api/routes/markdownRoute.js:1-30Validation
Code evidence
The title value is derived from request Markdown and inserted into the response without encoding.
Runtime evidence
A POST to
/api/v1/tools/markdown/previewwith a closing-title and script payload returned the payload inside the response title context.Recommended remediation
HTML-encode the extracted title before inserting it into the
<title>element, while retaining body sanitization.5. Sanitize Markdown-Rendered HTML in Blog Controller Before Output
ID:
SEC-005Severity: High
Category: Injection
Affected code:
api/controllers/blog.js:189,package.jsonImpact
GitHub-sourced Markdown can introduce executable HTML into blog pages because rendered content is emitted without sanitization.
Technical details
api/controllers/blog.js:189assignspost.contentHtml = marked.parse(post.content)without calling DOMPurify.api/views/pages/data/blog-post.ejs:46emitspost.contentHtmlwith the raw EJS operator<%- ... %>.[email protected]as the parser version.Relevant code
api/controllers/blog.js:183-195Validation
Code evidence
The blog controller renders Markdown directly, and the EJS template outputs the resulting HTML verbatim.
Runtime evidence
The supplied probe for
/data/blogs/MCU-orderconfirmed blog content was rendered as unescaped HTML.Limitations
The supplied evidence does not establish whether a particular live post currently contains an executable script.
Recommended remediation
Sanitize the output of
marked.parse()with the existing DOMPurify dependency before assigningcontentHtml, and address the reported outdated parser dependency.6. Escape JWT Claim Values Before Setting innerHTML in Auth Page
ID:
SEC-006Severity: High
Category: Injection
Affected code:
api/public/js/auth.jsImpact
HTML in JWT claims can execute when token details are displayed, allowing a forged or compromised same-origin token to trigger script in the auth page.
Technical details
api/public/js/auth.js:269–288interpolatespayload.userIdandpayload.emailinto a template assigned totokenDetails.innerHTML.textContentnor an escaping function.Relevant code
api/public/js/auth.js:1-30Validation
Code evidence
JWT payload fields are inserted directly into the
innerHTMLtemplate without sanitization.Runtime evidence
The signup endpoint rejected an HTML-formatted email, returning an invalid-email error.
Limitations
Server-side email validation mitigates the registration vector, but does not protect against localStorage poisoning through another same-origin XSS.
Recommended remediation
Render JWT claim values with
textContent, or HTML-escape each value before interpolation into any HTML context.7. Escape Third-Party Closings Data Before Injecting into innerHTML
ID:
SEC-007Severity: High
Category: Injection
Affected code:
api/public/js/closings.jsImpact
Malicious or compromised closings data can be rendered as HTML or script in every visitor's browser.
Technical details
api/public/js/closings.js:115–136assigns a template literal containingclosing.organization,closing.name,closing.region,closing.delay_time, andclosing.reasontoclosingsList.innerHTML.escapeHtml()being used by sibling scripts.Relevant code
api/public/js/closings.js:1-30Validation
Code evidence
External closings fields flow directly into an
innerHTMLsink without escaping.Runtime evidence
GET /api/v1/world/closingsreturned empty arrays during the supplied probe.Limitations
The empty response prevented live demonstration with a malicious closings value; the vulnerable rendering path remains reachable.
Recommended remediation
Apply
escapeHtml()to every closings field before interpolation, or construct the output with safe DOM APIs andtextContent.8. Block Private and Loopback Addresses in URL Shortener to Prevent SSRF and Open Redirect
ID:
SEC-008Severity: High
Category: Broken Access Control
Affected code:
api/routes/mainRoute.js:670–676,api/controllers/url.js:68–74Impact
The unauthenticated URL shortener accepts redirects to loopback, private, and link-local destinations, enabling open redirects and potentially facilitating server-side requests to internal services by consumers or integrations.
Technical details
api/controllers/url.js:68–74validates the destination syntactically withnew URL()but does not block private, loopback, or link-local addresses.api/routes/mainRoute.js:675redirects directly tourl.originalUrl.isURL()validation also permits these destinations.Relevant code
api/controllers/url.js:1-30Validation
Code evidence
No destination blocklist or scheme restriction is shown before the redirect.
Runtime evidence
A POST creating a short URL for
http://169.254.169.254/latest/meta-data/returned HTTP 200 and a short URL.Limitations
The supplied evidence demonstrates acceptance and redirect behavior, not a successful metadata-service retrieval by this application.
Recommended remediation
At creation time, allow only intended schemes and reject localhost, loopback, link-local, RFC-1918, and other private or reserved address ranges. Revalidate destinations before redirecting.
9. Migrate Authentication Token Storage from localStorage to HttpOnly Cookies
ID:
SEC-009Severity: Medium
Category: Cryptographic Failures
Affected code:
api/public/js/auth.js:56,97,api/public/js/auth-cookies.js:33–34Impact
JWTs stored in JavaScript-accessible storage can be exfiltrated by any XSS executing on the origin. The cookie migration does not remove this exposure because tokens remain in localStorage and the cookie is set client-side.
Technical details
api/public/js/auth.js:56stores login tokens withlocalStorage.setItem('authToken', result.data.token); the signup path at line 97 does the same.auth-cookies.jsalso writes the token to localStorage and sets the cookie throughdocument.cookie, so it is not HttpOnly.getToken()reads from the cookie or localStorage.Relevant code
api/public/js/auth.js:50-62Validation
Code evidence
The supplied code shows concurrent localStorage writes and a localStorage fallback after the cookie migration.
Limitations
No live token-exfiltration probe was supplied.
Recommended remediation
Move token storage to server-set cookies with
HttpOnly,Secure, andSameSite=Strictattributes, then remove all localStorage token reads and writes.10. Eliminate Timing-Based Email Enumeration in Login by Running bcrypt on Missing Accounts
ID:
SEC-010Severity: Low
Category: Authentication Failures
Affected code:
api/controllers/auth.jsImpact
Login response timing can reveal whether an email corresponds to an account, enabling user enumeration through repeated measurements.
Technical details
api/controllers/auth.js:109–111throws immediately whenUser.findOne({ email })returns no user.bcrypt.compare()at line 114 runs only for existing users, adding password-hashing work to that path.Relevant code
api/controllers/auth.js:103-117Validation
Code evidence
The missing-user branch performs no bcrypt comparison, while the existing-user branch does.
Runtime evidence
A nonexistent-account login returned in approximately 0.66 seconds; the supplied analysis identifies an additional approximately 100 ms bcrypt delay for an existing account with a wrong password.
Limitations
The runtime measurement was network-dominated, and repeated comparative sampling was not supplied.
Recommended remediation
When no user is found, perform a bcrypt comparison against a fixed dummy hash before returning the same generic authentication error.