'77712.34', 'floor' => '66611.22', 'margin' => '55510.99', 'bom_line_price' => '44409.87', 'category' => 'SENTINEL-CATEGORY-GPU', 'notes_internal' => 'SENTINEL-INTERNAL-NEVER-SHOW', 'cost_per_u' => '33308.76', ]; // The whitelisted payload (exactly renderData()'s shape) … $safe = [ 'quoteNo' => 'Q999-1', 'customer' => ['company' => 'Acme Corp', 'contact' => 'Jane Doe', 'email' => 'jane@acme.test', 'phone' => ''], 'specSummary' => 'Dual E5-2680v4 / 256 GB RAM / 4× 1.92TB SSD', 'specLines' => ['CPU: Dual E5-2680v4', 'RAM: 256 GB', 'Storage: 4× 1.92TB SSD'], 'location' => 'Houston, TX', 'monthly' => '289.00', 'setupFee' => '0.00', 'term' => '12 months', 'validUntil' => '2026-10-15', 'repName' => 'Rep Example', 'repEmail' => 'rep@dartnode.com', 'generated' => 'Sep 22, 2026', 'qty' => 1, ]; // …polluted with everything internal, as if a future bug passed the whole // snapshot into the template. The template must not print ANY of it. $polluted = array_merge($safe, [ 'cost' => $SENTINELS['cost'], 'costFloor' => $SENTINELS['floor'], 'floor_monthly' => $SENTINELS['floor'], 'margin' => $SENTINELS['margin'], 'bom' => [['name' => 'RAM', 'unit_price' => $SENTINELS['bom_line_price']]], 'category' => ['name' => $SENTINELS['category']], 'notes_internal' => $SENTINELS['notes_internal'], 'cost_per_u' => $SENTINELS['cost_per_u'], 'formula' => ['baseMonthly' => $SENTINELS['cost']], ]); // Structural check on the whitelist itself. foreach (['cost', 'floor', 'margin', 'bom', 'category', 'notes_internal', 'snapshot', 'formula'] as $k) { t("whitelist has no '{$k}' key", !array_key_exists($k, $safe)); } // Render the real template with bare Twig (same engine the app uses). $loader = new \Twig\Loader\FilesystemLoader(__DIR__ . '/../pages/templates'); $twig = new \Twig\Environment($loader, ['autoescape' => 'html']); $html = $twig->render('reports/sales-quote/index.twig', $polluted); foreach ($SENTINELS as $what => $needle) { t("template never leaks {$what}", strpos($html, $needle) === false); } // Sanity: the customer-facing values ARE present (template actually renders). t('renders monthly price', strpos($html, '289.00') !== false); t('renders quote number', strpos($html, 'Q999-1') !== false); t('renders valid-until', strpos($html, '2026-10-15') !== false); t('renders rep contact', strpos($html, 'rep@dartnode.com') !== false); echo $fails === 0 ? "\nALL PASS\n" : "\n{$fails} FAILURE(S)\n"; exit($fails === 0 ? 0 : 1);