mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Chore]: Add stricter sanitizing
This commit is contained in:
parent
b9ab513157
commit
785de21a79
10 changed files with 92 additions and 29 deletions
|
|
@ -8,7 +8,7 @@ function responseFormatter(req, res, next) {
|
||||||
* @param {number} statusCode - HTTP status code (default: 200)
|
* @param {number} statusCode - HTTP status code (default: 200)
|
||||||
*/
|
*/
|
||||||
// Determine mode: 'normal', 'debug', or 'ux-debug'
|
// Determine mode: 'normal', 'debug', or 'ux-debug'
|
||||||
let mode = 'normal';
|
let mode = 'production';
|
||||||
const config = req.app && req.app.get ? req.app.get('config') : null;
|
const config = req.app && req.app.get ? req.app.get('config') : null;
|
||||||
if (config && config.uxDebugMode) {
|
if (config && config.uxDebugMode) {
|
||||||
mode = 'ux-debug';
|
mode = 'ux-debug';
|
||||||
|
|
@ -22,6 +22,9 @@ function responseFormatter(req, res, next) {
|
||||||
mode: mode,
|
mode: mode,
|
||||||
data: data
|
data: data
|
||||||
};
|
};
|
||||||
|
if (req.sanitizedInput) {
|
||||||
|
response.sanitized = req.sanitizedInput;
|
||||||
|
}
|
||||||
if (templateContext) response.templateContext = templateContext;
|
if (templateContext) response.templateContext = templateContext;
|
||||||
res.status(statusCode).json(response);
|
res.status(statusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +42,9 @@ function responseFormatter(req, res, next) {
|
||||||
error: message,
|
error: message,
|
||||||
code: code
|
code: code
|
||||||
};
|
};
|
||||||
|
if (req.sanitizedInput) {
|
||||||
|
response.sanitized = req.sanitizedInput;
|
||||||
|
}
|
||||||
if (templateContext) response.templateContext = templateContext;
|
if (templateContext) response.templateContext = templateContext;
|
||||||
res.status(statusCode).json(response);
|
res.status(statusCode).json(response);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
infrastructure/web/api/middleware/sanitize.js
Normal file
51
infrastructure/web/api/middleware/sanitize.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
// Simple recursive sanitizer middleware for API input
|
||||||
|
// Strips HTML tags from all string fields in req.body, req.query, req.params
|
||||||
|
|
||||||
|
function stripHtml(str) {
|
||||||
|
if (typeof str !== 'string') return str;
|
||||||
|
// Remove all HTML tags
|
||||||
|
return str.replace(/<[^>]*>/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeObject(obj, sanitized = {}, path = '') {
|
||||||
|
let changed = false;
|
||||||
|
for (const key in obj) {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
||||||
|
const value = obj[key];
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const clean = stripHtml(value);
|
||||||
|
if (clean !== value) {
|
||||||
|
changed = true;
|
||||||
|
sanitized[path + key] = clean;
|
||||||
|
obj[key] = clean;
|
||||||
|
}
|
||||||
|
} else if (typeof value === 'object' && value !== null) {
|
||||||
|
// Recurse into objects/arrays
|
||||||
|
const subSanitized = {};
|
||||||
|
if (sanitizeObject(value, subSanitized, path + key + '.')) {
|
||||||
|
changed = true;
|
||||||
|
Object.assign(sanitized, subSanitized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeMiddleware(req, res, next) {
|
||||||
|
const sanitized = {};
|
||||||
|
let changed = false;
|
||||||
|
if (req.body && typeof req.body === 'object') {
|
||||||
|
if (sanitizeObject(req.body, sanitized, 'body.')) changed = true;
|
||||||
|
}
|
||||||
|
if (req.query && typeof req.query === 'object') {
|
||||||
|
if (sanitizeObject(req.query, sanitized, 'query.')) changed = true;
|
||||||
|
}
|
||||||
|
if (req.params && typeof req.params === 'object') {
|
||||||
|
if (sanitizeObject(req.params, sanitized, 'params.')) changed = true;
|
||||||
|
}
|
||||||
|
// Attach sanitized info to request for later use in response
|
||||||
|
if (changed) req.sanitizedInput = sanitized;
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = sanitizeMiddleware;
|
||||||
|
|
@ -8,6 +8,7 @@ const { errorHandler } = require('./middleware/error-handler')
|
||||||
* Main API Router (v1)
|
* Main API Router (v1)
|
||||||
* Mounts all API endpoints under /api/v1
|
* Mounts all API endpoints under /api/v1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function createApiRouter(dependencies) {
|
function createApiRouter(dependencies) {
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const { apiTokenRepository } = dependencies
|
const { apiTokenRepository } = dependencies
|
||||||
|
|
@ -20,6 +21,9 @@ function createApiRouter(dependencies) {
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Sanitize all input
|
||||||
|
router.use(require('./middleware/sanitize'))
|
||||||
|
|
||||||
// Response formatting helpers
|
// Response formatting helpers
|
||||||
router.use(responseFormatter)
|
router.use(responseFormatter)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,14 @@
|
||||||
<div id="account" class="account-container">
|
<div id="account" class="account-container">
|
||||||
<h1 class="page-title">Account Dashboard</h1>
|
<h1 class="page-title">Account Dashboard</h1>
|
||||||
<p class="account-subtitle">Welcome back, <strong>{{ username }}</strong></p>
|
<p class="account-subtitle">Welcome back, <strong>{{ username }}</strong></p>
|
||||||
|
<p class="account-subtitle">Welcome back, <strong>{{ username|sanitizeHtml }}</strong></p>
|
||||||
|
|
||||||
{% if successMessage %}
|
{% if successMessage %}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
<path d="M20 6L9 17l-5-5"></path>
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<p>{{ successMessage }}</p>
|
<p>{{ successMessage|sanitizeHtml }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
@ -35,7 +36,7 @@
|
||||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
</svg>
|
</svg>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage|sanitizeHtml }}</p>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
@ -72,7 +73,7 @@
|
||||||
{% for email in forwardEmails %}
|
{% for email in forwardEmails %}
|
||||||
<li class="email-item">
|
<li class="email-item">
|
||||||
<div class="email-info">
|
<div class="email-info">
|
||||||
<span class="email-address">{{ email.email }}</span>
|
<span class="email-address">{{ email.email|sanitizeHtml }}</span>
|
||||||
<span class="email-meta">Verified {{ email.verifiedAgo }}</span>
|
<span class="email-meta">Verified {{ email.verifiedAgo }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/account/forward-email/remove" class="inline-form">
|
<form method="POST" action="/account/forward-email/remove" class="inline-form">
|
||||||
|
|
@ -106,7 +107,7 @@
|
||||||
{% for inbox in lockedInboxes %}
|
{% for inbox in lockedInboxes %}
|
||||||
<li class="inbox-item">
|
<li class="inbox-item">
|
||||||
<div class="inbox-info">
|
<div class="inbox-info">
|
||||||
<a href="/inbox/{{ inbox.address }}" class="inbox-address">{{ inbox.address }}</a>
|
<a href="/inbox/{{ inbox.address }}" class="inbox-address">{{ inbox.address|sanitizeHtml }}</a>
|
||||||
<span class="inbox-meta">Last accessed {{ inbox.lastAccessedAgo }}</span>
|
<span class="inbox-meta">Last accessed {{ inbox.lastAccessedAgo }}</span>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="/account/locked-inbox/release" class="inline-form">
|
<form method="POST" action="/account/locked-inbox/release" class="inline-form">
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
</svg>
|
</svg>
|
||||||
{{ errorMessage }}
|
{{ errorMessage|sanitizeHtml }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if successMessage %}
|
{% if successMessage %}
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline;vertical-align:middle;margin-right:0.5rem">
|
||||||
<path d="M20 6L9 17l-5-5"></path>
|
<path d="M20 6L9 17l-5-5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
{{ successMessage }}
|
{{ successMessage|sanitizeHtml }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -57,6 +57,7 @@
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
pattern="[a-zA-Z0-9_]+"
|
pattern="[a-zA-Z0-9_]+"
|
||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
|
value="{{ username|sanitizeHtml }}"
|
||||||
>
|
>
|
||||||
<small>Letters, numbers, underscore only</small>
|
<small>Letters, numbers, underscore only</small>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1 class="page-title">{{message}}</h1>
|
<h1 class="page-title">{{message|sanitizeHtml}}</h1>
|
||||||
<h2>{{error.status}}</h2>
|
<h2>{{error.status|sanitizeHtml}}</h2>
|
||||||
<pre>{{error.stack}}</pre>
|
<pre>{{error.stack|sanitizeHtml}}</pre>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="inbox-container">
|
<div class="inbox-container">
|
||||||
<div class="inbox-header">
|
<div class="inbox-header">
|
||||||
<h1 class="inbox-title" id="copyAddress" title="Click to copy address">{{ address }}</h1>
|
<h1 class="inbox-title" id="copyAddress" title="Click to copy address">{{ address|sanitizeHtml }}</h1>
|
||||||
<button id="qrCodeBtn" class="qr-icon-btn" title="Show QR Code" aria-label="Show QR Code">
|
<button id="qrCodeBtn" class="qr-icon-btn" title="Show QR Code" aria-label="Show QR Code">
|
||||||
<svg class="qr-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<svg class="qr-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
<rect x="3" y="3" width="7" height="7"/>
|
<rect x="3" y="3" width="7" height="7"/>
|
||||||
|
|
@ -95,13 +95,13 @@
|
||||||
<div class="email-card frosted-glass">
|
<div class="email-card frosted-glass">
|
||||||
<div class="email-header">
|
<div class="email-header">
|
||||||
<div class="email-sender">
|
<div class="email-sender">
|
||||||
<div class="sender-name">{{ mail.from[0].name }}</div>
|
<div class="sender-name">{{ mail.from[0].name|sanitizeHtml }}</div>
|
||||||
<div class="sender-email">{{ mail.from[0].address }}</div>
|
<div class="sender-email">{{ mail.from[0].address|sanitizeHtml }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="email-date" data-date="{{ mail.date|date('c') }}"></div>
|
<div class="email-date" data-date="{{ mail.date|date('c') }}"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="email-subject-row">
|
<div class="email-subject-row">
|
||||||
<div class="email-subject">{{ mail.subject }}</div>
|
<div class="email-subject">{{ mail.subject|sanitizeHtml }}</div>
|
||||||
<div class="email-expiry">
|
<div class="email-expiry">
|
||||||
<span class="expiry-timer" data-date="{{ mail.date|date('c') }}">Expires in ...</span>
|
<span class="expiry-timer" data-date="{{ mail.date|date('c') }}">Expires in ...</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
<span class="close" id="closeQr">×</span>
|
<span class="close" id="closeQr">×</span>
|
||||||
<h3>Scan Email Address</h3>
|
<h3>Scan Email Address</h3>
|
||||||
<div id="qrcode" class="qr-code-container"></div>
|
<div id="qrcode" class="qr-code-container"></div>
|
||||||
<p class="qr-address-label">{{ address }}</p>
|
<p class="qr-address-label">{{ address|sanitizeHtml }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
<form method="POST" action="/" class="inbox-form">
|
<form method="POST" action="/" class="inbox-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="nameField">Choose Your Name</label>
|
<label for="nameField">Choose Your Name</label>
|
||||||
<input type="text" id="nameField" name="username" value="{{ username }}" placeholder="e.g., john.doe" required>
|
<input type="text" id="nameField" name="username" value="{{ username|sanitizeHtml }}" placeholder="e.g., john.doe" required>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -63,7 +63,7 @@
|
||||||
<div class="select-wrapper">
|
<div class="select-wrapper">
|
||||||
<select id="commentField" name="domain">
|
<select id="commentField" name="domain">
|
||||||
{% for domain in domains %}
|
{% for domain in domains %}
|
||||||
<option value="{{ domain }}">@{{ domain }}</option>
|
<option value="{{ domain|sanitizeHtml }}">@{{ domain|sanitizeHtml }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,9 +64,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mail-container">
|
<div class="mail-container">
|
||||||
<div class="mail-header">
|
<div class="mail-header">
|
||||||
<h1 class="mail-subject">{{ mail.subject }}</h1>
|
<h1 class="mail-subject">{{ mail.subject|sanitizeHtml }}</h1>
|
||||||
<div class="mail-meta">
|
<div class="mail-meta">
|
||||||
<div class="mail-from">From: {{ mail.from.text }}</div>
|
<div class="mail-from">From: {{ mail.from.text|sanitizeHtml }}</div>
|
||||||
<div class="mail-date" data-date="{{ mail.date|date('c') }}"></div>
|
<div class="mail-date" data-date="{{ mail.date|date('c') }}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -104,10 +104,10 @@
|
||||||
{% for crypto in cryptoAttachments %}
|
{% for crypto in cryptoAttachments %}
|
||||||
<div class="crypto-item">
|
<div class="crypto-item">
|
||||||
<div class="crypto-item-header">
|
<div class="crypto-item-header">
|
||||||
<span class="crypto-type">{{ crypto.type }}</span>
|
<span class="crypto-type">{{ crypto.type|sanitizeHtml }}</span>
|
||||||
<span class="crypto-filename">{{ crypto.filename }}{% if crypto.info %} · {{ crypto.info }}{% endif %}</span>
|
<span class="crypto-filename">{{ crypto.filename|sanitizeHtml }}{% if crypto.info %} · {{ crypto.info|sanitizeHtml }}{% endif %}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="crypto-key-content">{{ crypto.content }}</pre>
|
<pre class="crypto-key-content">{{ crypto.content|sanitizeHtml }}</pre>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,7 +120,7 @@
|
||||||
<div class="attachments-list">
|
<div class="attachments-list">
|
||||||
{% for attachment in mail.attachments %}
|
{% for attachment in mail.attachments %}
|
||||||
<a href="/inbox/{{ address }}/{{ uid }}/{{ attachment.checksum }}" class="attachment-link">
|
<a href="/inbox/{{ address }}/{{ uid }}/{{ attachment.checksum }}" class="attachment-link">
|
||||||
📎 {{ attachment.filename }}
|
📎 {{ attachment.filename|sanitizeHtml }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,8 @@
|
||||||
{% if stats.enhanced.topSenderDomains|length > 0 %}
|
{% if stats.enhanced.topSenderDomains|length > 0 %}
|
||||||
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
|
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
|
||||||
<li class="stat-list-item">
|
<li class="stat-list-item">
|
||||||
<span class="stat-list-label">{{ item.domain }}</span>
|
<span class="stat-list-label">{{ item.domain|sanitizeHtml }}</span>
|
||||||
<span class="stat-list-value">{{ item.count }}</span>
|
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -120,8 +120,8 @@
|
||||||
{% if stats.enhanced.topRecipientDomains|length > 0 %}
|
{% if stats.enhanced.topRecipientDomains|length > 0 %}
|
||||||
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
|
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
|
||||||
<li class="stat-list-item">
|
<li class="stat-list-item">
|
||||||
<span class="stat-list-label">{{ item.domain }}</span>
|
<span class="stat-list-label">{{ item.domain|sanitizeHtml }}</span>
|
||||||
<span class="stat-list-value">{{ item.count }}</span>
|
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
@ -140,8 +140,8 @@
|
||||||
{% if stats.enhanced.busiestHours|length > 0 %}
|
{% if stats.enhanced.busiestHours|length > 0 %}
|
||||||
{% for item in stats.enhanced.busiestHours %}
|
{% for item in stats.enhanced.busiestHours %}
|
||||||
<li class="stat-list-item">
|
<li class="stat-list-item">
|
||||||
<span class="stat-list-label">{{ item.hour }}:00 - {{ item.hour + 1 }}:00</span>
|
<span class="stat-list-label">{{ item.hour|sanitizeHtml }}:00 - {{ (item.hour + 1)|sanitizeHtml }}:00</span>
|
||||||
<span class="stat-list-value">{{ item.count }}</span>
|
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue