Skip to content

[Security] 19 findings (1 Critical, 11 High, 7 Medium) #2

Description

@pvz122

Security report for Traviseric/teneo-marketplace

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 1dee886. 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 19 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 Critical Cryptographic Failures marketplace/frontend/brands/teneo/config.json:7
marketplace/backend/config/network.js:49
SEC-002 High Cryptographic Failures marketplace/backend/middleware/teneoAuth.js:112-119
marketplace/backend/middleware/teneoAuth.js:125-137
marketplace/backend/middleware/teneoAuth.js:112-119,125-137
SEC-003 High Injection marketplace/frontend/admin-dashboard.html
SEC-004 High Injection marketplace/backend/routes/storeBuilder.js
SEC-005 High Authentication Failures marketplace/backend/server.js
marketplace/backend/routes/adminRoutes.js
SEC-006 High Cryptographic Failures marketplace/frontend/cart.js
marketplace/frontend/checkout.html
marketplace/frontend/brands/teneo/brand-manager.html
marketplace/frontend/manage-books.html
marketplace/frontend/admin/lulu-manager.html
marketplace/frontend/js/*.js
SEC-007 High Injection marketplace/frontend/brands/master-templates/purchase-success.html
SEC-008 High Injection marketplace/frontend/js/product-page-enhanced.js:386
marketplace/frontend/js/success-stories.js:139,144,152,212
marketplace/frontend/brands/master-templates/main.js:368
marketplace/frontend/js/publisher-profile.js:106
funnel-module/frontend/js/course-player.js:339,347
marketplace/frontend/js/success-stories.js:144, 152, 212
publisher-profile.js:106
course-player.js:339,347
success-stories.js:139,144,152
SEC-009 High Injection marketplace/frontend/js/marketplace.js
SEC-010 High Injection marketplace/backend/services/storeRendererService.js
marketplace/backend/server.js
SEC-011 High Insecure Design marketplace/backend/routes/checkout.js
marketplace/backend/server.js
server.js
SEC-012 High Broken Access Control marketplace/backend/routes/checkout.js
SEC-013 Medium Broken Access Control marketplace/backend/routes/emailMarketing.js:412-434
marketplace/backend/routes/emailMarketing.js:412–434
SEC-014 Medium Injection marketplace/backend/routes/emailMarketing.js:458-462
marketplace/backend/routes/emailTracking.js
marketplace/backend/routes/emailMarketing.js:458, 461
SEC-015 Medium Broken Access Control marketplace/backend/server.js
marketplace/backend/routes/auth.js
SEC-016 Medium Broken Access Control funnel-module/frontend/js/funnel-builder.js
SEC-017 Medium Cryptographic Failures marketplace/backend/database/database.js:212
marketplace/backend/database/init.js:126
SEC-018 Medium Injection funnel-module/frontend/js/funnel-builder.js:934
SEC-019 Medium Mishandling of Exceptional Conditions marketplace/backend/routes/storefront.js
marketplace/backend/routes/merchantFulfillment.js
marketplace/backend/routes/adminRoutes.js
routes/storefront.js
routes/merchantFulfillment.js
routes/adminRoutes.js

1. Remove Hardcoded RSA Private Key from Source Control

ID: SEC-001
Severity: Critical
Category: Cryptographic Failures
Affected code: marketplace/frontend/brands/teneo/config.json:7, marketplace/backend/config/network.js:49

Impact

The RSA private key is exposed in a public, git-tracked configuration and can be used to forge federation signatures. Existing copies remain compromised even after removing the current file.

Technical details

  • The Teneo brand configuration contains a private_key value, and the supplied validation record identifies it as a full PKCS8 PEM RSA-2048 private key.
  • marketplace/backend/config/network.js:49 assigns config.private_key to this.privateKey for federation-message signing.

Relevant code

marketplace/frontend/brands/teneo/config.json:1-10

{
  "network_enabled": false,
  "share_catalog": false,
  "accept_referrals": false,
  "referral_percentage": 0,
  "public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsDBBNc1QuYFCesEXFvNq\nElu+/vm4xqJjItamekYKwYpA5llifPCS8rem4cj+DivUwXrFum8x6HcnlZgRrpcQ\nQ9M+XIFeDYwwsHYLgexpSL/OuxKB4bXrbWI7da+vDOTYtH4RDN5lT3t7cheEIaHQ\nh3kOP/luF25xz5ZaNvLBBG/PTSIHRsFnvu1AObVhPhpxNCQej7t7WGsQGwCrr2OH\nT2QXovMH+DTmJo4q4RbbYyxyKr2iHHu4egAsdc5IXsKClTcBWMGQU5+0VNcX3sWG\n5dFKF0eqQNiLMXjOBPMPn8wUzyl5jNVbdto3yDgYjQYre4NBKhk8trJAs+k3FPW1\n6wIDAQAB\n-----END PUBLIC KEY-----\n",
  "private_key": "[REDACTED_SECRET]",
  "network_peers": [],
  "trusted_stores": []
}

Validation

Code evidence

  • marketplace/frontend/brands/teneo/config.json:7 contains the private_key configuration field.

  • marketplace/backend/config/network.js:49 loads the configured private key directly.
    Limitations

  • The provided code snippet redacts the secret value, so the exact key material is not reproduced here.

  • No live probe was performed; the finding is based on repository evidence.

Recommended remediation

Remove the private key from the repository and purge it from git history. Revoke or rotate the exposed key, then load its replacement through runtime environment or secret-management injection.

2. Encrypt or Avoid Persisting Raw Access Tokens in Database

ID: SEC-002
Severity: High
Category: Cryptographic Failures
Affected code: marketplace/backend/middleware/teneoAuth.js:112-119, marketplace/backend/middleware/teneoAuth.js:125-137, marketplace/backend/middleware/teneoAuth.js:112-119,125-137

Impact

A database compromise exposes usable Teneo bearer tokens, allowing token replay against authenticated functionality.

Technical details

  • updateStoredToken inserts the supplied token directly into teneo_auth_tokens.access_token.
  • getStoredToken retrieves the token as plaintext; the supplied record identifies no hashing or encryption on this path.

Relevant code

marketplace/backend/middleware/teneoAuth.js:106-125

            }
            console.error('Error validating book ownership:', error.message);
            throw error;
        }
    }

    static async updateStoredToken(userId, token) {
        const query = `
            INSERT OR REPLACE INTO teneo_auth_tokens (user_id, access_token, updated_at)
            VALUES (?, ?, CURRENT_TIMESTAMP)
        `;
        
        try {
            await db.run(query, [userId, token]);
        } catch (error) {
            console.error('Error storing Teneo token:', error);
        }
    }

    static async getStoredToken(userId) {

Validation

Code evidence

  • marketplace/backend/middleware/teneoAuth.js:112–119 executes INSERT OR REPLACE ... VALUES (?, ?, ...) with [userId, token].

  • The supplied validation record identifies teneo_auth_tokens.access_token as TEXT NOT NULL and contrasts this with encrypted merchant credentials elsewhere.
    Limitations

  • The deployed /api/auth/status endpoint returned 404, so database behavior was not runtime-tested.

Recommended remediation

Store a keyed digest or managed-key encryption of access tokens, compare or decrypt only when required, apply an appropriate token lifetime, and prevent token values from being logged.

3. Fix Stored XSS in Admin Dashboard Order Rendering

ID: SEC-003
Severity: High
Category: Injection
Affected code: marketplace/frontend/admin-dashboard.html

Impact

Attacker-controlled order data can execute JavaScript in an administrator's browser when the admin dashboard renders affected orders.

Technical details

  • renderOrders() interpolates database fields such as customer_email, book_title, and payment_status into an innerHTML template without escaping.
  • The supplied record states that customer_name and other order fields originate from checkout-controlled data; an escapeHtml() helper exists in the file but is not applied to this renderer.

Validation

Code evidence

  • The supplied validation record locates the vulnerable interpolation in marketplace/frontend/admin-dashboard.html around lines 2923–2943.

  • The same record identifies an existing escapeHtml() helper used by another renderer, while renderOrders() leaves the listed fields unescaped.
    Limitations

  • No code snippet was supplied for the dashboard renderer.

  • The deployed API was reported as unavailable, so the end-to-end injection path was not runtime-tested.

Recommended remediation

Render untrusted order fields with textContent or equivalent safe DOM APIs. If HTML is required, apply context-appropriate allowlist sanitization before insertion.

4. Escape User-Supplied Values in Store Builder HTML Template Renderer

ID: SEC-004
Severity: High
Category: Injection
Affected code: marketplace/backend/routes/storeBuilder.js

Impact

Merchant-controlled storefront configuration can produce HTML containing script payloads, causing XSS for users who view the rendered or saved storefront.

Technical details

  • POST /api/store-builder/render accepts a caller-supplied config and passes it to renderStorePage without an authentication gate shown in the supplied route.
  • The renderer uses String(val) and raw template interpolation rather than HTML encoding values before constructing HTML.

Relevant code

marketplace/backend/routes/storeBuilder.js:56-77

  }
});

// POST /api/store-builder/render
// Body: { "config": <StoreConfig> }
// Returns: { success: true, html: "<html>..." }
router.post('/render', (req, res) => {
  const { config } = req.body;
  if (!config) return res.status(400).json({ error: 'config required' });
  try {
    const html = renderStorePage(config);
    res.json({ success: true, html });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/store-builder/generate-and-render
// Single call: brief → config → HTML
// Body: { "brief": "..." }
// Returns: { success: true, config, html }
router.post('/generate-and-render', async (req, res) => {

Validation

Code evidence

  • marketplace/backend/routes/storeBuilder.js:62–71 accepts req.body.config and returns the result of renderStorePage(config).

  • The supplied validation record identifies storeRendererService.js:35 and inline interpolations around lines 178, 252, 254, and 268 as unescaped HTML sinks.
    Limitations

  • The deployed render endpoint returned 404, so exploitation was not confirmed against the deployed instance.

Recommended remediation

HTML-escape all configuration values before insertion, or use safe DOM/template APIs. Sanitize explicitly allowed rich text with a server-side allowlist, and require authorization for store creation or rendering where appropriate.

5. Configure Persistent Session Store to Replace Insecure MemoryStore Default

ID: SEC-005
Severity: High
Category: Authentication Failures
Affected code: marketplace/backend/server.js, marketplace/backend/routes/adminRoutes.js

Impact

Sessions are held in process memory and disappear on restart. The admin login path also preserves the existing session identifier while granting admin state, creating a session-fixation risk.

Technical details

  • express-session is configured without a store option in server.js, so the default MemoryStore is used.
  • adminRoutes.js:121–123 sets req.session.isAdmin = true without calling req.session.regenerate(); the supplied record identifies regeneration in several other authentication paths.

Relevant code

marketplace/backend/server.js:1-30

// marketplace/backend/server.js
require('dotenv').config();

// Environment variable validation
function validateEnvironment() {
    if (process.env.NODE_ENV === 'production') {
        const fatal = [];
        if (!process.env.ADMIN_PASSWORD_HASH) {
            fatal.push('ADMIN_PASSWORD_HASH must be set in production. Run: node scripts/generate-password-hash.js --generate');
        }
        if (!process.env.SESSION_SECRET) {
            fatal.push('SESSION_SECRET must be set in production (min 32 chars)');
        } else if (process.env.SESSION_SECRET.length < 32) {
            fatal.push('SESSION_SECRET must be at least 32 characters in production');
        }
        if (fatal.length > 0) {
            console.error('FATAL: Missing required environment configuration:');
            fatal.forEach(msg => console.error(' -', msg));
            process.exit(1);
        }
    } else {
        if (!process.env.ADMIN_PASSWORD_HASH) {
            console.warn('⚠️  ADMIN_PASSWORD_HASH not set. Admin login will fail until configured.');
        }
        if (!process.env.SESSION_SECRET) {
            const crypto = require('crypto');
            process.env.SESSION_SECRET = crypto.randomBytes(64).toString('hex');
            console.warn('⚠️  Using auto-generated session secret (dev only). Set SESSION_SECRET in production!');
        }
    }

Validation

Code evidence

  • marketplace/backend/server.js:167–178 configures the session without store:.

  • The supplied validation record locates the admin privilege assignment at adminRoutes.js:121–123 and contrasts it with routes/auth.js regeneration calls.
    Limitations

  • The deployed admin API returned 404, so session behavior was not runtime-tested.

Recommended remediation

Configure a persistent, production-appropriate session store and call req.session.regenerate() before assigning authenticated or administrative session state.

6. Replace localStorage Credential Storage with HttpOnly Session Cookies and Clear Storage on Logout

ID: SEC-006
Severity: High
Category: Cryptographic Failures
Affected code: marketplace/frontend/cart.js, marketplace/frontend/checkout.html, marketplace/frontend/brands/teneo/brand-manager.html, marketplace/frontend/manage-books.html, marketplace/frontend/admin/lulu-manager.html, marketplace/frontend/js/*.js

Impact

Any JavaScript executing in the site origin can read persisted admin credentials or bearer tokens. Logout does not remove these values, leaving them available after the apparent session ends.

Technical details

  • manage-books.html stores book_manager_auth in localStorage, and other frontend code reads it back.
  • Multiple scripts read teneo_token from localStorage and use it as a Bearer token; the supplied record states that the logout flow does not clear localStorage or sessionStorage.

Relevant code

marketplace/frontend/checkout.html:1-30

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Checkout</title>
    <link rel="stylesheet" href="styles/variables.css">
    <link rel="stylesheet" href="styles/base.css">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
            background: #F9FAFB;
            color: #111827;
            line-height: 1.6;
            min-height: 100vh;
        }

        header {
            background: rgba(255,255,255,0.95);
            border-bottom: 1px solid #E5E7EB;
            padding: 1.25rem 0;
            backdrop-filter: blur(10px);
        }

        .header-inner {
            max-width: 900px;
            margin: 0 auto;
            padding: 0 24px;

Validation

Code evidence

  • The supplied validation record locates localStorage.setItem('book_manager_auth', authToken) in manage-books.html and reads in lulu-manager.html.

  • It also identifies Bearer-token reads in book-performance-modal.js, rewards.js, and published-dashboard.js, plus logout code in auth.js that redirects without clearing storage.
    Runtime evidence

  • The deployed manage-books.html page reportedly contained localStorage.getItem('book_manager_auth').
    Limitations

  • The provided snippet is from checkout.html and does not include the cited storage operations.

Recommended remediation

Move authentication state to appropriately scoped, Secure, HttpOnly cookies and remove bearer credentials from localStorage. Clear remaining client-side authentication state during logout.

7. Replace document.write of URL Parameter with Safe DOM Insertion

ID: SEC-007
Severity: High
Category: Injection
Affected code: marketplace/frontend/brands/master-templates/purchase-success.html

Impact

A crafted amount query parameter can inject HTML and script into the purchase-success page, causing reflected client-side XSS when the page is opened.

Technical details

  • The page reads amount with URLSearchParams and writes it through document.write() without validation or encoding.
  • A value containing markup can close the surrounding element and introduce additional HTML, including script elements.

Relevant code

marketplace/frontend/brands/master-templates/purchase-success.html:1-30

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Purchase Complete - {{BRAND_NAME}}</title>
    <meta name="description" content="Thank you for your purchase. Your order is being processed.">
    
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    
    <!-- Font Awesome -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    
    <!-- Design System Base -->
    <link rel="stylesheet" href="../../styles/variables.css">
    <link rel="stylesheet" href="../../styles/base.css">
    <!-- Custom Styles -->
    <style>
        :root {
            --primary-color: {{PRIMARY_COLOR|#1E40AF}};
            --accent-color: {{ACCENT_COLOR|#F59E0B}};
            --success-color: #10B981;
        }
        
        .success-animation {
            animation: successPulse 0.6s ease-out;
        }
        
        @keyframes successPulse {

Validation

Code evidence

  • The supplied validation record locates document.write(new URLSearchParams(window.location.search).get('amount') || '19.99') at line 125 of marketplace/frontend/brands/master-templates/purchase-success.html.
    Runtime evidence

  • The supplied record reports that the deployed page returned HTTP 200 and contained the vulnerable document.write call.
    Limitations

  • The supplied runtime observation confirms the vulnerable code was served, not that a script payload executed.

Recommended remediation

Replace document.write() with a specific DOM element assignment using textContent, and validate amount as the expected numeric format before displaying it.

8. Replace innerHTML Assignments with Safe DOM APIs in Frontend Renderers

ID: SEC-008
Severity: High
Category: Injection
Affected code: marketplace/frontend/js/product-page-enhanced.js:386, marketplace/frontend/js/success-stories.js:139,144,152,212, marketplace/frontend/brands/master-templates/main.js:368, marketplace/frontend/js/publisher-profile.js:106, funnel-module/frontend/js/course-player.js:339,347, marketplace/frontend/js/success-stories.js:144, 152, 212, publisher-profile.js:106, course-player.js:339,347, success-stories.js:139,144,152

Impact

Untrusted API-sourced or stored profile and content fields can execute JavaScript in visitors' browsers when rendered through unsafe innerHTML assignments.

Technical details

  • publisher-profile.js inserts profile.profile_image_url and displayName into an HTML template without escaping or URL validation.
  • The supplied record identifies similar unescaped interpolations for success-story and tips fields; it excludes one static product-page-enhanced.js occurrence from the confirmed data flow.

Relevant code

marketplace/frontend/js/publisher-profile.js:1-30

class PublisherProfile {
    constructor() {
        this.userId = this.getUserIdFromUrl();
        this.currentUser = null;
        this.profile = null;
        this.badges = {
            'bronze_book': { icon: '📘', name: 'Bronze Book', color: '#cd7f32' },
            'silver_stack': { icon: '📚', name: 'Silver Stack', color: '#c0c0c0' },
            'gold_trophy': { icon: '🏆', name: 'Gold Trophy', color: '#ffd700' },
            'diamond': { icon: '💎', name: 'Diamond', color: '#b9f2ff' },
            'crown': { icon: '👑', name: 'Crown', color: '#ff6b6b' },
            'rocket': { icon: '🚀', name: 'Rocket', color: '#4ecdc4' },
            'star': { icon: '🌟', name: 'Star', color: '#ffe66d' }
        };
        
        this.init();
    }

    getUserIdFromUrl() {
        const pathParts = window.location.pathname.split('/');
        const profileIndex = pathParts.indexOf('profile');
        return profileIndex !== -1 && pathParts[profileIndex + 1] ? pathParts[profileIndex + 1] : null;
    }

    async init() {
        if (!this.userId) {
            this.showError('Invalid profile URL');
            return;
        }

Validation

Code evidence

  • The supplied validation record cites publisher-profile.js:106: avatarContainer.innerHTML = \${displayName}``.

  • It also cites success-stories.js around lines 124, 128, 139, and 212 for API fields inserted into innerHTML.
    Runtime evidence

  • The deployed site reportedly returned HTTP 200.
    Limitations

  • The backend API returned 404 in the supplied validation record, so the profile-update-to-execution chain was not runtime-tested.

  • No exploit execution was demonstrated.

Recommended remediation

Use textContent for text fields, validate URL schemes and destinations before assigning URL attributes, and sanitize any intentionally supported markup with an allowlist.

9. Escape Book Title and Description Before innerHTML Assignment in Marketplace Grid

ID: SEC-009
Severity: High
Category: Injection
Affected code: marketplace/frontend/js/marketplace.js

Impact

Poisoned catalog fields can execute JavaScript for marketplace visitors when catalog cards are rendered.

Technical details

  • createBookCard() interpolates book.title, description, author, badge, and other fields into a template assigned to grid.innerHTML without escaping.
  • The supplied record states that authenticated admin book-management code writes caller-supplied book data to the catalog, which is then served to users.

Relevant code

marketplace/frontend/js/marketplace.js:145-159

            <div class="book-card" data-book-id="${book.id}">
                <a href="/book-detail.html?id=${book.id}&brand=${currentBrand}" class="book-link">
                    <img src="${book.coverImage}" alt="${book.title}" class="book-cover" 
                         onerror="this.src='https://via.placeholder.com/400x600?text=${encodeURIComponent(book.title)}'">
                    <div class="book-info">
                        ${book.badge ? `<span class="book-badge">${book.badge}</span>` : ''}
                        <h3 class="book-title">${book.title}</h3>
                        <p class="book-author">${book.author}</p>
                        <p class="book-description">${book.description}</p>
                        <div class="book-price">
                            <span class="current-price">$${book.price}</span>
                            ${book.originalPrice ? `<span class="original-price">$${book.originalPrice}</span>` : ''}
                        </div>
                    </div>
                </a>

Validation

Code evidence

  • marketplace/frontend/js/marketplace.js:145–159 places fields such as ${book.title}, ${book.author}, and ${book.description} inside HTML assigned to the grid.
    Runtime evidence

  • The deployed root page reportedly contains the active books-grid element used by client-side catalog rendering.
    Limitations

  • The supplied evidence does not include a live poisoned catalog or script-execution result.

Recommended remediation

Build cards with DOM APIs and assign untrusted text through textContent. Validate URL attributes separately, and sanitize any intentionally supported rich text with an allowlist.

10. Escape Dynamic Fields in Server-Side Storefront Template Renderer

ID: SEC-010
Severity: High
Category: Injection
Affected code: marketplace/backend/services/storeRendererService.js, marketplace/backend/server.js

Impact

Merchant-controlled store and product values can become stored XSS in storefront responses and affect storefront visitors.

Technical details

  • fillTemplate converts values with String(val) without HTML encoding.
  • The supplied record identifies raw interpolations of store name, tagline, product name, description, and ID in renderer fallbacks; stored HTML is served by /store/:slug without sanitization.

Relevant code

marketplace/backend/services/storeRendererService.js:1-30

/**
 * Store Renderer Service
 *
 * Converts a store_config JSON (produced by aiStoreBuilderService) into a complete HTML page
 * using the component library in marketplace/frontend/components-library/.
 *
 * Usage:
 *   const { renderStorePage } = require('./storeRendererService');
 *   const html = renderStorePage(storeConfig);
 *
 * Works without ANTHROPIC_API_KEY — pure template rendering, no AI calls.
 */

const fs = require('fs');
const path = require('path');

const COMPONENTS_DIR = path.join(__dirname, '../../frontend/components-library');

/**
 * Load a component HTML file. Returns empty string if the file is a stub or missing.
 */
function loadComponent(relPath) {
  const fullPath = path.join(COMPONENTS_DIR, relPath);
  if (!fs.existsSync(fullPath)) return '';
  const content = fs.readFileSync(fullPath, 'utf8').trim();
  // Stub components are just a comment line (e.g. "<!-- Book Card (individual) -->")
  if (content.length < 100) return '';
  return content;
}

Validation

Code evidence

  • marketplace/backend/services/storeRendererService.js contains the renderer described above; the supplied validation record locates fillTemplate around lines 35–39 and additional raw interpolations around lines 105–107, 129–131, and 252–254.

  • The record states that /store/:slug sends stored HTML with res.send(store.html).
    Limitations

  • The deployed store-builder and store routes returned 404, so the stored-XSS path was not runtime-tested.

Recommended remediation

Apply context-aware HTML escaping to all dynamic template values, or sanitize explicitly supported rich text with a server-side allowlist before storage and response.

11. Register Stripe Webhook Route Before Global Body-Parser to Prevent Signature Bypass

ID: SEC-011
Severity: High
Category: Insecure Design
Affected code: marketplace/backend/routes/checkout.js, marketplace/backend/server.js, server.js

Impact

If the global JSON parser consumes the webhook body before the Stripe route's raw-body middleware, signature verification may fail or behave incorrectly, creating a potential forged-webhook risk.

Technical details

  • The supplied record states that bodyParser.json() is mounted globally before checkout routes.
  • The checkout webhook then calls stripe.webhooks.constructEvent(req.body, sig, webhookSecret); the supplied evidence says req.body may already be a parsed object rather than the original raw bytes.

Relevant code

marketplace/backend/routes/checkout.js:1-30

const express = require('express');
const router = express.Router();
const crypto = require('crypto');
const rateLimit = require('express-rate-limit');
const { safeMessage, sanitizeMetadataValue, isValidEmail } = require('../utils/validate');
const path = require('path');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const btcpayService = require('../services/btcpayService');
const arxmintService = require('../services/arxmintService');
const stripeHealthService = require('../services/stripeHealthService');
const {
  sanitizeBrandId,
  lookupBookPrice,
  applyCouponToPrice,
  getNextReadOffer,
  resolveCatalogItem
} = require('../services/checkoutOfferService');
const couponService = require('../services/couponService');
const orderBumpService = require('../services/orderBumpService');
const OrderService = require('../services/orderService');
const orderService = new OrderService();
const emailService = require('../services/emailService');
const { processMixedOrder } = require('./checkoutMixed');
const nftService = require('../services/nftService');
const shippingService = require('../services/shippingService');
const db = require('../database/database');
const { enrollUserInCourse } = require('./courseRoutes');
const licenseKeyService = require('../services/licenseKeyService');
const { trackReferral } = require('./referralRoutes');

Validation

Code evidence

  • The supplied validation record locates global bodyParser.json() at server.js:199, checkout route mounting at line 256, and per-route express.raw() near checkout.js:298.
    Limitations

  • The deployed webhook endpoint returned 404, so signature behavior was not runtime-tested.

  • The record describes a potential failure or bypass on affected stripe-node versions; no forged webhook was demonstrated.

Recommended remediation

Ensure the webhook route receives the untouched request bytes before any global JSON parser runs, and verify the Stripe signature over that raw body.

12. Scope Order Lookup to Authenticated User to Prevent IDOR

ID: SEC-012
Severity: High
Category: Broken Access Control
Affected code: marketplace/backend/routes/checkout.js

Impact

The order-status endpoint can disclose order details to callers who know or enumerate an order ID because the lookup is not scoped to an authenticated owner.

Technical details

  • GET /order/:orderId calls orderService.getOrder(orderId) without authentication middleware shown in the supplied route.
  • The supplied record states that the service query filters only by order_id and not by user_id.

Relevant code

marketplace/backend/routes/checkout.js:684-696

      }
    }
  }
}

// Get order status
router.get('/order/:orderId', async (req, res) => {
  try {
    const { orderId } = req.params;
    const order = await orderService.getOrder(orderId);

    if (!order) {
      return res.status(404).json({ error: 'Order not found' });

Validation

Code evidence

  • marketplace/backend/routes/checkout.js:690 defines the order route and calls getOrder(orderId).

  • The supplied validation record identifies the underlying query as SELECT * FROM orders WHERE order_id = ?.
    Limitations

  • The deployed order endpoint returned 404, so unauthorized retrieval was not runtime-tested.

  • The record contains conflicting descriptions of authentication; the supplied route evidence supports that no authentication middleware is shown.

Recommended remediation

Require authentication and add an ownership predicate such as the authenticated user's identifier to the order query. Return the same not-found response when the order is absent or not owned by the caller.

13. Validate Redirect URL Parameter in Email Marketing Endpoint to Prevent Open Redirect

ID: SEC-013
Severity: Medium
Category: Broken Access Control
Affected code: marketplace/backend/routes/emailMarketing.js:412-434, marketplace/backend/routes/emailMarketing.js:412–434

Impact

An unauthenticated attacker can use the click-tracking endpoint to redirect visitors to an arbitrary external destination, supporting phishing and brand impersonation.

Technical details

  • GET /track/click/:sendId assigns req.query.url to destination and passes it directly to res.redirect(302, destination).
  • The handler performs no URL parsing, protocol validation, hostname validation, or allowlist check.

Relevant code

marketplace/backend/routes/emailMarketing.js:406-440

        // Non-fatal — pixel already sent
    }
});

// GET /api/email-marketing/track/click/:sendId?url=https://...
// Logs a click then redirects to the destination URL
router.get('/track/click/:sendId', async (req, res) => {
    const destination = req.query.url;
    const sendId = parseInt(req.params.sendId, 10);

    // Record asynchronously then redirect
    if (Number.isFinite(sendId) && destination) {
        try {
            await dbRun(
                `UPDATE email_sends SET clicked_at = COALESCE(clicked_at, CURRENT_TIMESTAMP) WHERE id = ?`,
                [sendId]
            );
            await dbRun(
                `INSERT INTO email_events (event_type, send_id, url, ip_address, user_agent)
                 VALUES ('click', ?, ?, ?, ?)`,
                [sendId, destination, req.ip || null, req.get('user-agent') || null]
            );
        } catch (e) {
            // Non-fatal
        }
    }

    if (destination) {
        res.redirect(302, destination);
    } else {
        res.status(400).send('Missing url parameter');
    }
});

// ─── Unsubscribe (public) ─────────────────────────────────────────────────────

Validation

Code evidence

  • marketplace/backend/routes/emailMarketing.js:412–434: const destination = req.query.url; when present, it is passed to res.redirect(302, destination).

  • The supplied record identifies emailTracking.js:131–143 as an in-repository implementation that parses and validates redirect URLs.
    Runtime evidence

  • The route is mounted at /api/email-marketing/track/click/:sendId according to the supplied validation record.
    Limitations

  • The deployed URL returned 404 for the route, so live redirect behavior was not confirmed.

Recommended remediation

Parse the value with URL, permit only the intended protocols and destinations, and reject invalid or disallowed targets before calling res.redirect.

14. HTML-Encode Subscriber Email Before Embedding in Server-Rendered Unsubscribe Response

ID: SEC-014
Severity: Medium
Category: Injection
Affected code: marketplace/backend/routes/emailMarketing.js:458-462, marketplace/backend/routes/emailTracking.js, marketplace/backend/routes/emailMarketing.js:458, 461

Impact

A malicious subscriber email can be stored and later reflected as HTML in the unsubscribe response, enabling XSS for anyone opening the confirmation page.

Technical details

  • The supplied record states that the unsubscribe handler interpolates subscriber.email directly into an HTML response at emailMarketing.js:461.
  • The subscription validation regex accepts HTML metacharacters, allowing malicious content to reach storage after trimming and lowercasing.

Relevant code

marketplace/backend/routes/emailMarketing.js:1-30

/**
 * Email Marketing Management Routes
 *
 * All operations use the shared DB adapter (SQLite local / Supabase in production).
 *
 * Subscriber Management (admin):
 *   GET  /api/email-marketing/subscribers          - list subscribers with pagination
 *   GET  /api/email-marketing/subscribers/:id      - get subscriber details
 *   DELETE /api/email-marketing/subscribers/:id    - remove subscriber
 *
 * Email Sequences (admin):
 *   GET  /api/email-marketing/sequences            - list sequences
 *   POST /api/email-marketing/sequences            - create sequence
 *   PUT  /api/email-marketing/sequences/:id        - update sequence
 *   DELETE /api/email-marketing/sequences/:id      - delete sequence
 *   GET  /api/email-marketing/sequences/:id/emails - list sequence emails
 *   POST /api/email-marketing/sequences/:id/emails - add email to sequence
 *
 * Broadcasts (admin):
 *   GET  /api/email-marketing/broadcasts           - list broadcasts
 *   POST /api/email-marketing/broadcasts           - create broadcast
 *
 * Unsubscribe (public):
 *   GET  /api/email-marketing/unsubscribe/:token   - unsubscribe via token link
 */

const express = require('express');
const router = express.Router();
const { authenticateAdmin } = require('../middleware/auth');
const { dbRun, dbGet, dbAll } = require('../services/databaseHelper');

Validation

Code evidence

  • The supplied validation record cites emailTracking.js:30 regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ as accepting HTML metacharacters.

  • It cites marketplace/backend/routes/emailMarketing.js:461 as interpolating ${subscriber.email} into res.send(...) without encoding.
    Limitations

  • The deployed unsubscribe endpoint returned 404, so the stored-and-reflected execution chain was not runtime-tested.

Recommended remediation

Use strict email validation, HTML-encode the email before embedding it in the response, and preferably construct the confirmation response with a safe templating mechanism.

15. Remove Logout Route from CSRF Exclusion List

ID: SEC-015
Severity: Medium
Category: Broken Access Control
Affected code: marketplace/backend/server.js, marketplace/backend/routes/auth.js

Impact

A cross-site request can terminate a user's authenticated session because logout is excluded from the application's CSRF protection.

Technical details

  • /api/auth/logout is listed in csrfExcludePaths.
  • The logout handler destroys the session, while the supplied record states that the authentication check does not provide Origin, Referer, or CSRF-token validation.

Relevant code

marketplace/backend/server.js:1-30

// marketplace/backend/server.js
require('dotenv').config();

// Environment variable validation
function validateEnvironment() {
    if (process.env.NODE_ENV === 'production') {
        const fatal = [];
        if (!process.env.ADMIN_PASSWORD_HASH) {
            fatal.push('ADMIN_PASSWORD_HASH must be set in production. Run: node scripts/generate-password-hash.js --generate');
        }
        if (!process.env.SESSION_SECRET) {
            fatal.push('SESSION_SECRET must be set in production (min 32 chars)');
        } else if (process.env.SESSION_SECRET.length < 32) {
            fatal.push('SESSION_SECRET must be at least 32 characters in production');
        }
        if (fatal.length > 0) {
            console.error('FATAL: Missing required environment configuration:');
            fatal.forEach(msg => console.error(' -', msg));
            process.exit(1);
        }
    } else {
        if (!process.env.ADMIN_PASSWORD_HASH) {
            console.warn('⚠️  ADMIN_PASSWORD_HASH not set. Admin login will fail until configured.');
        }
        if (!process.env.SESSION_SECRET) {
            const crypto = require('crypto');
            process.env.SESSION_SECRET = crypto.randomBytes(64).toString('hex');
            console.warn('⚠️  Using auto-generated session secret (dev only). Set SESSION_SECRET in production!');
        }
    }

Validation

Code evidence

  • marketplace/backend/server.js:215 excludes /api/auth/logout from CSRF protection.

  • The supplied validation record cites auth.js:445–452 for the authentication check and states that POST /logout calls req.session.destroy().
    Limitations

  • The deployed logout endpoint returned 404, so forced logout was not runtime-tested.

Recommended remediation

Remove logout from the CSRF exclusion list and protect the state-changing request with the application's CSRF token mechanism or strict Origin/Referer validation.

16. Validate returnUrl Parameter Before Assigning to window.location in Funnel Builder

ID: SEC-016
Severity: Medium
Category: Broken Access Control
Affected code: funnel-module/frontend/js/funnel-builder.js

Impact

When the funnel module is served, a crafted returnUrl can redirect users to an arbitrary external destination; the supplied record also identifies acceptance of javascript: values.

Technical details

  • parseURLContext() stores the query parameter returnUrl without validation.
  • window.location.href is later assigned this.context.returnUrl without checking protocol or origin.

Relevant code

funnel-module/frontend/js/funnel-builder.js:1-30

// Funnel Builder - Core Logic
// Integrates with template-processor.js for variable replacement

class FunnelBuilder {
  constructor() {
    this.selectedTemplate = null;
    this.variables = {};
    this.context = null;
    this.autoSaveInterval = null;
    this.templateProcessor = null;
    this.aiPrompts = null;

    this.init();
  }

  async init() {
    console.log('Initializing Funnel Builder...');

    // Initialize template processor
    if (typeof TemplateProcessor !== 'undefined') {
      this.templateProcessor = new TemplateProcessor();
    }

    // Load AI prompts
    await this.loadAIPrompts();

    // Parse URL context (course integration)
    this.context = this.parseURLContext();

    // Setup UI

Validation

Code evidence

  • The supplied validation record cites funnel-module/frontend/js/funnel-builder.js:71 for raw returnUrl capture and line 911 for assignment to window.location.href.
    Limitations

  • The funnel module is not included in the supplied deployment configuration and the deployed page returned 404; only static code evidence is available.

Recommended remediation

Parse the value with new URL(value, location.origin), require the intended protocol and origin, and reject external or otherwise invalid destinations before navigation.

17. Enable TLS Certificate Validation for PostgreSQL Connections

ID: SEC-017
Severity: Medium
Category: Cryptographic Failures
Affected code: marketplace/backend/database/database.js:212, marketplace/backend/database/init.js:126

Impact

When PostgreSQL TLS is enabled, the client accepts any server certificate, allowing a man-in-the-middle to impersonate the database endpoint and intercept or alter database traffic.

Technical details

  • database.js enables SSL when PGSSLMODE=require, PGSSL=true, or the connection string matches supabase.co.
  • The resulting configuration sets rejectUnauthorized: false, disabling certificate verification.

Relevant code

marketplace/backend/database/database.js:206-218

    const databaseUrl = process.env.DATABASE_URL || process.env.SUPABASE_DB_URL;
    const sslRequested = process.env.PGSSLMODE === 'require' || process.env.PGSSL === 'true' || /supabase\.co/i.test(databaseUrl || '');

    const pool = new Pool({
        connectionString: databaseUrl,
        ssl: sslRequested ? { rejectUnauthorized: false } : undefined,
    });

    pool.on('error', (err) => {
        console.error('[DB] Unexpected PostgreSQL pool error:', err.message);
    });

Validation

Code evidence

  • marketplace/backend/database/database.js:206–218 constructs the pool with ssl: sslRequested ? { rejectUnauthorized: false } : undefined.

  • The supplied finding also identifies the same setting in marketplace/backend/database/init.js:126.
    Runtime evidence

  • The deployed application reportedly responded to an HTTP health URL, but this did not test database certificate validation.
    Limitations

  • No database connection or man-in-the-middle test was performed.

Recommended remediation

Enable certificate verification for PostgreSQL connections and configure the required trusted CA material rather than disabling rejectUnauthorized.

18. Sanitize Funnel Template HTML Before Writing to Preview Window via document.write

ID: SEC-018
Severity: Medium
Category: Injection
Affected code: funnel-module/frontend/js/funnel-builder.js:934

Impact

Funnel template content containing attacker-controlled markup or scripts is written into a new browser window and can execute in the preview context.

Technical details

  • fullscreenPreview() obtains processed template HTML and passes it directly to win.document.write(processedHTML).
  • The supplied record states that variable values originate from form input and are substituted into the template without sanitization before this write.

Relevant code

funnel-module/frontend/js/funnel-builder.js:928-940

    document.getElementById(`preview-${mode}`)?.classList.add('active');
  }

  fullscreenPreview() {
    const processedHTML = this.getProcessedHTML();
    const win = window.open('', '_blank');
    win.document.write(processedHTML);
    win.document.close();
  }

  showNotification(message, type = 'success') {
    const toast = document.getElementById('notification-toast');
    const icon = document.getElementById('notification-icon');

Validation

Code evidence

  • funnel-module/frontend/js/funnel-builder.js:934 calls win.document.write(processedHTML) and then closes the document.
    Limitations

  • The funnel module returned 404 on the deployed URL, so no live payload execution was observed.

  • The supplied record characterizes the demonstrated path as self-XSS unless drafts are shared.

Recommended remediation

Do not write untrusted HTML directly with document.write(). Sanitize permitted template content with a strict allowlist before previewing, or render through safe DOM APIs and isolate the preview context.

19. Reject Array-Valued Query Parameters to Prevent HTTP Parameter Pollution DoS and Injection

ID: SEC-019
Severity: Medium
Category: Mishandling of Exceptional Conditions
Affected code: marketplace/backend/routes/storefront.js, marketplace/backend/routes/merchantFulfillment.js, marketplace/backend/routes/adminRoutes.js, routes/storefront.js, routes/merchantFulfillment.js, routes/adminRoutes.js

Impact

Duplicate query parameters can trigger unhandled type errors on public routes and produce incorrect path or query handling on other routes, enabling request-level denial of service or logic errors.

Technical details

  • Express can represent duplicate query keys as arrays, but the supplied handlers use values without type checks.
  • storefront.js calls req.query.category.toLowerCase(), while the supplied record identifies an unvalidated brand_id passed to path.join() in adminRoutes.js.

Relevant code

marketplace/backend/routes/storefront.js:1-30

/**
 * Storefront API — Standardized catalog and fulfillment endpoints.
 *
 * This is the API that external consumers (like ArxMint's /bazaar page) use
 * to fetch product catalogs and trigger order fulfillment.
 *
 * Endpoints:
 *   GET  /api/storefront/catalog           - Full product catalog (standardized schema)
 *   GET  /api/storefront/catalog/:category  - Products filtered by category
 *   GET  /api/storefront/product/:id        - Single product detail
 *   POST /api/storefront/checkout           - Create checkout session (routes to payment provider)
 *   POST /api/storefront/fulfill            - Webhook: payment confirmed → fulfill order
 *
 * Schema matches BAZAAR_STRATEGY.md Product interface.
 */

'use strict';

const express = require('express');
const router = express.Router();
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const axios = require('axios');
const arxmintProvider = require('../services/arxmintProvider');
const OrderService = require('../services/orderService');
const emailService = require('../services/emailService');
const fulfillmentService = require('../services/fulfillmentService');
const zapService = require('../services/zapService');

Validation

Code evidence

  • The supplied validation record locates the category call in marketplace/backend/routes/storefront.js:276.

  • It locates the path.join(..., brand, ...) use in adminRoutes.js:933–934 and states that duplicate brand_id values become arrays.
    Limitations

  • The deployed API returned 404, so duplicate-parameter behavior was not runtime-tested.

  • The supplied record identifies multiple affected files but provides concrete line-level behavior only for the cited storefront and admin paths.

Recommended remediation

Validate that each query parameter is a single string before string, path, or database operations. Reject arrays and duplicate values with a client error, and handle validation failures without uncaught exceptions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions