[Chore]: Add stricter sanitizing

This commit is contained in:
ClaraCrazy 2026-01-06 14:52:06 +01:00
parent b9ab513157
commit 785de21a79
No known key found for this signature in database
GPG key ID: EBBC896ACB497011
10 changed files with 92 additions and 29 deletions

View file

@ -8,7 +8,7 @@ function responseFormatter(req, res, next) {
* @param {number} statusCode - HTTP status code (default: 200)
*/
// 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;
if (config && config.uxDebugMode) {
mode = 'ux-debug';
@ -22,6 +22,9 @@ function responseFormatter(req, res, next) {
mode: mode,
data: data
};
if (req.sanitizedInput) {
response.sanitized = req.sanitizedInput;
}
if (templateContext) response.templateContext = templateContext;
res.status(statusCode).json(response);
}
@ -39,6 +42,9 @@ function responseFormatter(req, res, next) {
error: message,
code: code
};
if (req.sanitizedInput) {
response.sanitized = req.sanitizedInput;
}
if (templateContext) response.templateContext = templateContext;
res.status(statusCode).json(response);
}

View 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;

View file

@ -8,6 +8,7 @@ const { errorHandler } = require('./middleware/error-handler')
* Main API Router (v1)
* Mounts all API endpoints under /api/v1
*/
function createApiRouter(dependencies) {
const router = express.Router()
const { apiTokenRepository } = dependencies
@ -20,6 +21,9 @@ function createApiRouter(dependencies) {
allowedHeaders: ['Content-Type', 'Authorization']
}))
// Sanitize all input
router.use(require('./middleware/sanitize'))
// Response formatting helpers
router.use(responseFormatter)

View file

@ -18,13 +18,14 @@
<div id="account" class="account-container">
<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|sanitizeHtml }}</strong></p>
{% if successMessage %}
<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">
<path d="M20 6L9 17l-5-5"></path>
</svg>
<p>{{ successMessage }}</p>
<p>{{ successMessage|sanitizeHtml }}</p>
</div>
{% endif %}
@ -35,7 +36,7 @@
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
<p>{{ errorMessage }}</p>
<p>{{ errorMessage|sanitizeHtml }}</p>
</div>
{% endif %}
@ -72,7 +73,7 @@
{% for email in forwardEmails %}
<li class="email-item">
<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>
</div>
<form method="POST" action="/account/forward-email/remove" class="inline-form">
@ -106,7 +107,7 @@
{% for inbox in lockedInboxes %}
<li class="inbox-item">
<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>
</div>
<form method="POST" action="/account/locked-inbox/release" class="inline-form">

View file

@ -26,7 +26,7 @@
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
{{ errorMessage }}
{{ errorMessage|sanitizeHtml }}
</div>
{% endif %}
{% 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">
<path d="M20 6L9 17l-5-5"></path>
</svg>
{{ successMessage }}
{{ successMessage|sanitizeHtml }}
</div>
{% endif %}
</div>
@ -57,6 +57,7 @@
maxlength="20"
pattern="[a-zA-Z0-9_]+"
autocomplete="username"
value="{{ username|sanitizeHtml }}"
>
<small>Letters, numbers, underscore only</small>

View file

@ -32,7 +32,7 @@
{% endblock %}
{% block body %}
<h1 class="page-title">{{message}}</h1>
<h2>{{error.status}}</h2>
<pre>{{error.stack}}</pre>
<h1 class="page-title">{{message|sanitizeHtml}}</h1>
<h2>{{error.status|sanitizeHtml}}</h2>
<pre>{{error.stack|sanitizeHtml}}</pre>
{% endblock %}

View file

@ -77,7 +77,7 @@
{% endif %}
<div class="inbox-container">
<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">
<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"/>
@ -95,13 +95,13 @@
<div class="email-card frosted-glass">
<div class="email-header">
<div class="email-sender">
<div class="sender-name">{{ mail.from[0].name }}</div>
<div class="sender-email">{{ mail.from[0].address }}</div>
<div class="sender-name">{{ mail.from[0].name|sanitizeHtml }}</div>
<div class="sender-email">{{ mail.from[0].address|sanitizeHtml }}</div>
</div>
<div class="email-date" data-date="{{ mail.date|date('c') }}"></div>
</div>
<div class="email-subject-row">
<div class="email-subject">{{ mail.subject }}</div>
<div class="email-subject">{{ mail.subject|sanitizeHtml }}</div>
<div class="email-expiry">
<span class="expiry-timer" data-date="{{ mail.date|date('c') }}">Expires in ...</span>
</div>
@ -173,7 +173,7 @@
<span class="close" id="closeQr">&times;</span>
<h3>Scan Email Address</h3>
<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>

View file

@ -55,7 +55,7 @@
<form method="POST" action="/" class="inbox-form">
<div class="form-group">
<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 class="form-group">
@ -63,7 +63,7 @@
<div class="select-wrapper">
<select id="commentField" name="domain">
{% for domain in domains %}
<option value="{{ domain }}">@{{ domain }}</option>
<option value="{{ domain|sanitizeHtml }}">@{{ domain|sanitizeHtml }}</option>
{% endfor %}
</select>
</div>

View file

@ -64,9 +64,9 @@
{% endif %}
<div class="mail-container">
<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-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>
</div>
@ -104,10 +104,10 @@
{% for crypto in cryptoAttachments %}
<div class="crypto-item">
<div class="crypto-item-header">
<span class="crypto-type">{{ crypto.type }}</span>
<span class="crypto-filename">{{ crypto.filename }}{% if crypto.info %} · {{ crypto.info }}{% endif %}</span>
<span class="crypto-type">{{ crypto.type|sanitizeHtml }}</span>
<span class="crypto-filename">{{ crypto.filename|sanitizeHtml }}{% if crypto.info %} · {{ crypto.info|sanitizeHtml }}{% endif %}</span>
</div>
<pre class="crypto-key-content">{{ crypto.content }}</pre>
<pre class="crypto-key-content">{{ crypto.content|sanitizeHtml }}</pre>
</div>
{% endfor %}
</div>
@ -120,7 +120,7 @@
<div class="attachments-list">
{% for attachment in mail.attachments %}
<a href="/inbox/{{ address }}/{{ uid }}/{{ attachment.checksum }}" class="attachment-link">
📎 {{ attachment.filename }}
📎 {{ attachment.filename|sanitizeHtml }}
</a>
{% endfor %}
</div>

View file

@ -100,8 +100,8 @@
{% if stats.enhanced.topSenderDomains|length > 0 %}
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
<li class="stat-list-item">
<span class="stat-list-label">{{ item.domain }}</span>
<span class="stat-list-value">{{ item.count }}</span>
<span class="stat-list-label">{{ item.domain|sanitizeHtml }}</span>
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
</li>
{% endfor %}
{% else %}
@ -120,8 +120,8 @@
{% if stats.enhanced.topRecipientDomains|length > 0 %}
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
<li class="stat-list-item">
<span class="stat-list-label">{{ item.domain }}</span>
<span class="stat-list-value">{{ item.count }}</span>
<span class="stat-list-label">{{ item.domain|sanitizeHtml }}</span>
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
</li>
{% endfor %}
{% else %}
@ -140,8 +140,8 @@
{% if stats.enhanced.busiestHours|length > 0 %}
{% for item in stats.enhanced.busiestHours %}
<li class="stat-list-item">
<span class="stat-list-label">{{ item.hour }}:00 - {{ item.hour + 1 }}:00</span>
<span class="stat-list-value">{{ item.count }}</span>
<span class="stat-list-label">{{ item.hour|sanitizeHtml }}:00 - {{ (item.hour + 1)|sanitizeHtml }}:00</span>
<span class="stat-list-value">{{ item.count|sanitizeHtml }}</span>
</li>
{% endfor %}
{% else %}