diff --git a/infrastructure/web/api/middleware/response-formatter.js b/infrastructure/web/api/middleware/response-formatter.js index 761bcaf..6d3a9a9 100644 --- a/infrastructure/web/api/middleware/response-formatter.js +++ b/infrastructure/web/api/middleware/response-formatter.js @@ -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); } diff --git a/infrastructure/web/api/middleware/sanitize.js b/infrastructure/web/api/middleware/sanitize.js new file mode 100644 index 0000000..8c01b06 --- /dev/null +++ b/infrastructure/web/api/middleware/sanitize.js @@ -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; \ No newline at end of file diff --git a/infrastructure/web/api/router.js b/infrastructure/web/api/router.js index 7b469c5..39a565e 100644 --- a/infrastructure/web/api/router.js +++ b/infrastructure/web/api/router.js @@ -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) diff --git a/infrastructure/web/views/account.twig b/infrastructure/web/views/account.twig index e5f3b8c..f5adc3b 100644 --- a/infrastructure/web/views/account.twig +++ b/infrastructure/web/views/account.twig @@ -18,13 +18,14 @@

Account Dashboard

Welcome back, {{ username }}

+

Welcome back, {{ username|sanitizeHtml }}

{% if successMessage %}
-

{{ successMessage }}

+

{{ successMessage|sanitizeHtml }}

{% endif %} @@ -35,7 +36,7 @@ -

{{ errorMessage }}

+

{{ errorMessage|sanitizeHtml }}

{% endif %} @@ -72,7 +73,7 @@ {% for email in forwardEmails %}
  • - +
    @@ -106,7 +107,7 @@ {% for inbox in lockedInboxes %}
  • - {{ inbox.address }} + {{ inbox.address|sanitizeHtml }} Last accessed {{ inbox.lastAccessedAgo }}
    diff --git a/infrastructure/web/views/auth.twig b/infrastructure/web/views/auth.twig index 1d05527..730430a 100644 --- a/infrastructure/web/views/auth.twig +++ b/infrastructure/web/views/auth.twig @@ -26,7 +26,7 @@ - {{ errorMessage }} + {{ errorMessage|sanitizeHtml }} {% endif %} {% if successMessage %} @@ -34,7 +34,7 @@ - {{ successMessage }} + {{ successMessage|sanitizeHtml }} {% endif %} @@ -57,6 +57,7 @@ maxlength="20" pattern="[a-zA-Z0-9_]+" autocomplete="username" + value="{{ username|sanitizeHtml }}" > Letters, numbers, underscore only diff --git a/infrastructure/web/views/error.twig b/infrastructure/web/views/error.twig index d181b4d..60d3051 100644 --- a/infrastructure/web/views/error.twig +++ b/infrastructure/web/views/error.twig @@ -32,7 +32,7 @@ {% endblock %} {% block body %} -

    {{message}}

    -

    {{error.status}}

    -
    {{error.stack}}
    +

    {{message|sanitizeHtml}}

    +

    {{error.status|sanitizeHtml}}

    +
    {{error.stack|sanitizeHtml}}
    {% endblock %} diff --git a/infrastructure/web/views/inbox.twig b/infrastructure/web/views/inbox.twig index fa1d6da..e470fdf 100644 --- a/infrastructure/web/views/inbox.twig +++ b/infrastructure/web/views/inbox.twig @@ -77,7 +77,7 @@ {% endif %}
    -

    {{ address }}

    +

    {{ address|sanitizeHtml }}