A
Austin Songer, CISSP
Guest
- Thread Author
- #1
poc.md
This solution provides a fully automated AWS-native mechanism to monitor, detect, and alert on unauthorized changes to page scripts. It leverages AWS CloudFront, Lambda@Edge, and the Hono framework to seamlessly enforce content integrity and real-time monitoring for scripts that are served as part of processing pages.
Key Features
Critical Pages Where Script Integrity is Important
https://gist.github.com/#--indexjs
import { Hono } from 'hono';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
import crypto from 'crypto';
// Setup AWS clients
const REGION = 'us-east-1';
const ssm = new SSMClient({ region: REGION });
const sns = new SNSClient({ region: REGION });
const TAMPER_ALERT_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:ScriptTamperAlerts';
const HASH_PARAM_NAME = '/tampering/known-good-script-hashes';
const CSP_HEADER_VALUE = "script-src 'self' https://trusted-cdn.example.com; report-uri https://example.com/csp-report";
// In-memory cache for known-good hashes
let knownHashes = null;
// Load known-good hashes from Parameter Store
async function loadKnownHashes() {
if (knownHashes) return knownHashes;
const command = new GetParameterCommand({ Name: HASH_PARAM_NAME });
const response = await ssm.send(command);
knownHashes = JSON.parse(response.Parameter.Value);
return knownHashes;
}
// Calculate SHA-384 hash for SRI
function calculateHash(content) {
return 'sha384-' + crypto.createHash('sha384').update(content, 'utf8').digest('base64');
}
// Send tamper alert to SNS
async function sendTamperAlert(script, expected, actual) {
const message = `TAMPER DETECTED: ${script}\nExpected: ${expected}\nActual: ${actual}`;
console.error(message);
const command = new PublishCommand({
TopicArn: TAMPER_ALERT_TOPIC_ARN,
Subject: 'Script Tamper Alert',
Message: message,
});
await sns.send(command);
}
// Fetch script from origin (adjust if using custom origins)
async function fetchFromOrigin(path) {
const originUrl = `https://your-origin-bucket.s3.amazonaws.com${path}`;
const response = await fetch(originUrl);
return await response.text();
}
// Hono app setup
const app = new Hono();
// Middleware to inject CSP Header
app.use('*', async (c, next) => {
c.res.headers.set('Content-Security-Policy', CSP_HEADER_VALUE);
await next();
});
// Handle JavaScript files
app.get('*.js', async (c) => {
const path = c.req.path;
const scriptContent = await fetchFromOrigin(path);
const scriptHash = calculateHash(scriptContent);
const hashes = await loadKnownHashes();
const scriptName = path.split('/').pop();
const expectedHash = hashes[scriptName];
console.log(`Serving ${path} with hash ${scriptHash}`);
if (expectedHash && expectedHash !== scriptHash) {
await sendTamperAlert(path, expectedHash, scriptHash);
}
return new Response(scriptContent, { headers: { 'Content-Type': 'application/javascript' } });
});
export default app;
zip function.zip index.js
aws lambda create-function \
--function-name hono-singlefile \
--runtime nodejs18.x \
--role arn:aws:iam::123456789012:role/lambda-edge-role \
--handler index.default \
--zip-file fileb://function.zip \
--region us-east-1
aws cloudfront update-distribution \
--id YOUR_CLOUDFRONT_DISTRIBUTION_ID \
--default-cache-behavior '{
"LambdaFunctionAssociations": [
{
"EventType": "origin-response",
"LambdaFunctionARN": "'$(aws lambda get-function --function-name hono-singlefile --query "Configuration.FunctionArn" --output text):$LATEST'"
}
]
}'
Imports / Dependencies
import { Hono } from 'hono';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
import crypto from 'crypto';
Configuration / Constants
const REGION = 'us-east-1';
const ssm = new SSMClient({ region: REGION });
const sns = new SNSClient({ region: REGION });
const TAMPER_ALERT_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:ScriptTamperAlerts';
const HASH_PARAM_NAME = '/pci-dss/known-good-script-hashes';
const CSP_HEADER_VALUE = "script-src 'self' https://trusted-cdn.example.com; report-uri https://example.com/csp-report";
Cache and Load Known-Good Hashes
let knownHashes = null;
async function loadKnownHashes() {
if (knownHashes) return knownHashes;
const command = new GetParameterCommand({ Name: HASH_PARAM_NAME });
const response = await ssm.send(command);
knownHashes = JSON.parse(response.Parameter.Value);
return knownHashes;
}
Hash Calculator
function calculateHash(content) {
return 'sha384-' + crypto.createHash('sha384').update(content, 'utf8').digest('base64');
}
Tamper Detection Alert
async function sendTamperAlert(script, expected, actual) {
const message = `TAMPER DETECTED: ${script}\nExpected: ${expected}\nActual: ${actual}`;
console.error(message);
const command = new PublishCommand({
TopicArn: TAMPER_ALERT_TOPIC_ARN,
Subject: 'Script Tamper Alert',
Message: message,
});
await sns.send(command);
}
Fetch the Actual Script from S3 (or Origin)
async function fetchFromOrigin(path) {
const originUrl = `https://your-origin-bucket.s3.amazonaws.com${path}`;
const response = await fetch(originUrl);
return await response.text();
}
Initialize the Hono App
const app = new Hono();
CSP Header Middleware
app.use('*', async (c, next) => {
c.res.headers.set('Content-Security-Policy', CSP_HEADER_VALUE);
await next();
});
Main Route Handler — Script Integrity Check
app.get('*.js', async (c) => {
const path = c.req.path;
const scriptContent = await fetchFromOrigin(path);
const scriptHash = calculateHash(scriptContent);
const hashes = await loadKnownHashes();
const scriptName = path.split('/').pop();
const expectedHash = hashes[scriptName];
console.log(`Serving ${path} with hash ${scriptHash}`);
if (expectedHash && expectedHash !== scriptHash) {
await sendTamperAlert(path, expectedHash, scriptHash);
}
return new Response(scriptContent, { headers: { 'Content-Type': 'application/javascript' } });
});
Export Hono App
export default app;
Proof of Concept - Script Change & Tamper Detection for Webpages using Hono
https://gist.github.com/#proof-of-concept---script-change--tamper-detection-for-webpages-using-honoThis solution provides a fully automated AWS-native mechanism to monitor, detect, and alert on unauthorized changes to page scripts. It leverages AWS CloudFront, Lambda@Edge, and the Hono framework to seamlessly enforce content integrity and real-time monitoring for scripts that are served as part of processing pages.
| Feature | Description |
|---|---|
| Every time a script (.js) is served, its cryptographic hash is recalculated and compared to a known-good hash stored securely in AWS Systems Manager Parameter Store. | |
| A strict CSP header is injected into all page responses to enforce only trusted scripts are allowed to run in the user’s browser. | |
| The system uses SHA-384 hashes, compatible with SRI if browser-based integrity checks are later added. | |
| If any unauthorized modification is detected (added, modified, or replaced script), the system sends a real-time SNS alert to security teams or SIEM platforms. | |
| Every script served is logged, including its path and hash value, creating a tamper-resistant audit trail. | |
| This logic runs at the CloudFront Edge, ensuring there is no added latency for the origin and allowing global enforcement at scale. | |
| Works with S3 origins, ALB origins, or custom origins, making it suitable for both static and dynamic payment pages. |
Critical Pages Where Script Integrity is Important
| Type of Page | Why Script Integrity Matters |
|---|---|
| Login Pages | Unauthorized script changes could steal credentials (keylogging or credential harvesting). This maps to authentication security requirements in frameworks like SOC 2, ISO 27001, and NIST. |
| Customer Account Pages | If scripts on “My Account” or “Profile” pages are compromised, attackers could exfiltrate personal data (PII) — relevant to GDPR, HIPAA, and SOC 2. |
| Checkout Pages (non-card payment) | Even if payments aren’t made by card (e.g., Apple Pay, PayPal), checkout pages are still a high-value target. Protecting them is a best practice for e-commerce security. |
| Healthcare Patient Portals | Unauthorized scripts could steal sensitive Protected Health Information (PHI), mapping to HIPAA 164.312(c)(1) (Integrity). |
| Banking and Financial Apps | Financial institutions need to ensure all online banking pages are protected from tampering. This maps to GLBA, FFIEC, and ISO 27001 requirements. |
| Internal Admin Portals | Unauthorized changes to internal admin dashboards could allow privilege escalation or data manipulation. Relevant to SOC 2 CC6/7, ISO 27001 A.9 (Access Control). |
| Support/Case Management Pages | Unauthorized changes could expose support tickets, customer messages, or sensitive attachments. This is important for SOC 2 (confidentiality). |
| Document Upload Pages | Any page allowing users to upload important documents (e.g., tax forms, contracts, medical records) benefits from ensuring uploaded scripts are unmodified. |
index.js
https://gist.github.com/#--indexjsimport { Hono } from 'hono';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
import crypto from 'crypto';
// Setup AWS clients
const REGION = 'us-east-1';
const ssm = new SSMClient({ region: REGION });
const sns = new SNSClient({ region: REGION });
const TAMPER_ALERT_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:ScriptTamperAlerts';
const HASH_PARAM_NAME = '/tampering/known-good-script-hashes';
const CSP_HEADER_VALUE = "script-src 'self' https://trusted-cdn.example.com; report-uri https://example.com/csp-report";
// In-memory cache for known-good hashes
let knownHashes = null;
// Load known-good hashes from Parameter Store
async function loadKnownHashes() {
if (knownHashes) return knownHashes;
const command = new GetParameterCommand({ Name: HASH_PARAM_NAME });
const response = await ssm.send(command);
knownHashes = JSON.parse(response.Parameter.Value);
return knownHashes;
}
// Calculate SHA-384 hash for SRI
function calculateHash(content) {
return 'sha384-' + crypto.createHash('sha384').update(content, 'utf8').digest('base64');
}
// Send tamper alert to SNS
async function sendTamperAlert(script, expected, actual) {
const message = `TAMPER DETECTED: ${script}\nExpected: ${expected}\nActual: ${actual}`;
console.error(message);
const command = new PublishCommand({
TopicArn: TAMPER_ALERT_TOPIC_ARN,
Subject: 'Script Tamper Alert',
Message: message,
});
await sns.send(command);
}
// Fetch script from origin (adjust if using custom origins)
async function fetchFromOrigin(path) {
const originUrl = `https://your-origin-bucket.s3.amazonaws.com${path}`;
const response = await fetch(originUrl);
return await response.text();
}
// Hono app setup
const app = new Hono();
// Middleware to inject CSP Header
app.use('*', async (c, next) => {
c.res.headers.set('Content-Security-Policy', CSP_HEADER_VALUE);
await next();
});
// Handle JavaScript files
app.get('*.js', async (c) => {
const path = c.req.path;
const scriptContent = await fetchFromOrigin(path);
const scriptHash = calculateHash(scriptContent);
const hashes = await loadKnownHashes();
const scriptName = path.split('/').pop();
const expectedHash = hashes[scriptName];
console.log(`Serving ${path} with hash ${scriptHash}`);
if (expectedHash && expectedHash !== scriptHash) {
await sendTamperAlert(path, expectedHash, scriptHash);
}
return new Response(scriptContent, { headers: { 'Content-Type': 'application/javascript' } });
});
export default app;
What This Control Covers Across Frameworks
https://gist.github.com/#-what-this-control-covers-across-frameworks| Control Area | Explanation |
|---|---|
| Change Detection | Tracks any changes to critical page scripts. |
| Tamper Detection | Ensures scripts are not modified maliciously (formjacking, Magecart, etc.). |
| Audit Trail | Logs all script changes and detections (maps to audit and event logging controls). |
| Incident Response Trigger | Automatically sends alerts (helps with detection, response, and breach notification processes). |
| Integrity Protection | Ensures only approved, signed scripts are allowed (integrity). |
| Defense-in-Depth | Works with CSP and SRI for additional browser-based enforcement. |
- It addresses file integrity monitoring for scripts (relevant to many standards).
- It includes real-time logging and alerting (key to auditability and incident response).
- It fits into any Secure Software Development Life Cycle (SDLC) by adding controls at the delivery layer (edge/CDN).
Requirement Mapping
https://gist.github.com/#-requirement-mapping| Framework | Relevant Requirements |
|---|---|
| NIST 800-53 (Rev 5) | - SI-7 (Software, Firmware, and Information Integrity)- SI-4 (Information System Monitoring)- AU-2 (Auditable Events)- AU-6 (Audit Review and Reporting) |
| ISO 27001 (Annex A) | - A.12.2.1 (Control of Technical Vulnerabilities)- A.12.4.1 (Event Logging)- A.12.1.2 (Change Management)- A.14.2.4 (System Security Testing) |
| SOC 2 (TSC) | - CC7.1 (Detect and Monitor Unauthorized Changes)- CC7.2 (Identify Changes to Components)- CC6.6 (Prevent and Detect System Vulnerabilities) |
| CIS Controls v8 | - Control 2 (Inventory and Control of Software Assets)- Control 5 (Account Management - Integrity Monitoring)- Control 6 (Access Control Management)- Control 13 (Data Protection)- Control 14 (Security Awareness and Skills Training - Change Risk Awareness) |
| FedRAMP (Moderate) | - SI-7 (Software and Information Integrity)- SI-4 (System Monitoring)- CM-3 (Configuration Change Control)- AU-2 (Audit Events) |
| **HIPAA ** | - 164.312(c)(1) Integrity (Implement policies to protect electronic PHI from alteration/destruction)- 164.308(a)(5)(ii)(B) Protection from Malicious Software- 164.312(b) Audit Controls |
| Payment Card Industry (PCI DSS) | - 11.6.1 (The primary driver)- 6.2 (System Components Protection)- 10.3 (Audit Trail Requirements) |
Deployment Script
https://gist.github.com/#-deployment-scriptzip function.zip index.js
aws lambda create-function \
--function-name hono-singlefile \
--runtime nodejs18.x \
--role arn:aws:iam::123456789012:role/lambda-edge-role \
--handler index.default \
--zip-file fileb://function.zip \
--region us-east-1
aws cloudfront update-distribution \
--id YOUR_CLOUDFRONT_DISTRIBUTION_ID \
--default-cache-behavior '{
"LambdaFunctionAssociations": [
{
"EventType": "origin-response",
"LambdaFunctionARN": "'$(aws lambda get-function --function-name hono-singlefile --query "Configuration.FunctionArn" --output text):$LATEST'"
}
]
}'
Functionality
https://gist.github.com/#functionalityimport { Hono } from 'hono';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns';
import crypto from 'crypto';
| Line | What It Does |
|---|---|
| import { Hono } from 'hono'; | Pulls in Hono framework to handle routes and requests. This makes building Lambda@Edge handlers cleaner. |
| import { SSMClient, GetParameterCommand } | Loads AWS SDK v3 clients to talk to SSM Parameter Store, which stores the known-good hashes for payment scripts. |
| import { SNSClient, PublishCommand } | Loads AWS SDK v3 clients to talk to SNS, which will send tamper alerts if unauthorized changes are detected. |
| import crypto from 'crypto'; | Built-in Node.js library to calculate SHA-384 hashes for Subresource Integrity (SRI) checks. |
const REGION = 'us-east-1';
const ssm = new SSMClient({ region: REGION });
const sns = new SNSClient({ region: REGION });
const TAMPER_ALERT_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:ScriptTamperAlerts';
const HASH_PARAM_NAME = '/pci-dss/known-good-script-hashes';
const CSP_HEADER_VALUE = "script-src 'self' https://trusted-cdn.example.com; report-uri https://example.com/csp-report";
| What | What It Does |
|---|---|
| REGION | Sets AWS region to us-east-1, required for Lambda@Edge. |
| ssm / sns | Creates AWS SDK clients to interact with Parameter Store and SNS. |
| TAMPER_ALERT_TOPIC_ARN | The SNS Topic ARN that gets notified if tampering is detected. |
| HASH_PARAM_NAME | SSM Parameter name where known-good hashes are stored. |
| CSP_HEADER_VALUE | Sets a strict Content Security Policy (CSP) header to help browsers block unauthorized scripts. This is critical to PCI DSS 11.6.1. |
let knownHashes = null;
async function loadKnownHashes() {
if (knownHashes) return knownHashes;
const command = new GetParameterCommand({ Name: HASH_PARAM_NAME });
const response = await ssm.send(command);
knownHashes = JSON.parse(response.Parameter.Value);
return knownHashes;
}
| What | What It Does |
|---|---|
| knownHashes | Simple in-memory cache to avoid reloading hashes on every request. |
| loadKnownHashes() | - Talks to SSM Parameter Store to fetch the JSON list of trusted script hashes.- Example stored value: { "payment.js": "sha384-abc123..." } |
function calculateHash(content) {
return 'sha384-' + crypto.createHash('sha384').update(content, 'utf8').digest('base64');
}
| What | What It Does |
|---|---|
| calculateHash() | Hashes the content of a script using SHA-384, which is the recommended algorithm for Subresource Integrity (SRI). |
| Output Example | sha384-ABC123... |
async function sendTamperAlert(script, expected, actual) {
const message = `TAMPER DETECTED: ${script}\nExpected: ${expected}\nActual: ${actual}`;
console.error(message);
const command = new PublishCommand({
TopicArn: TAMPER_ALERT_TOPIC_ARN,
Subject: 'Script Tamper Alert',
Message: message,
});
await sns.send(command);
}
| What | What It Does |
|---|---|
| sendTamperAlert() | - Sends a formatted message to SNS.- This could trigger email, Slack, PagerDuty, etc.- Message contains the script name, expected hash, and actual hash. |
async function fetchFromOrigin(path) {
const originUrl = `https://your-origin-bucket.s3.amazonaws.com${path}`;
const response = await fetch(originUrl);
return await response.text();
}
| What | What It Does |
|---|---|
| fetchFromOrigin() | - Fetches the script file from your origin.- In this case, it assumes an S3 bucket, but you could point to any origin (e.g., ALB, EC2, etc.). |
const app = new Hono();
| What | What It Does |
|---|---|
| app | This is the main Hono instance, where all routes, middleware, and logic are attached. |
app.use('*', async (c, next) => {
c.res.headers.set('Content-Security-Policy', CSP_HEADER_VALUE);
await next();
});
| What | What It Does |
|---|---|
| Adds CSP | Every response (all paths) gets the strict CSP Header. This protects against malicious injected scripts. |
app.get('*.js', async (c) => {
const path = c.req.path;
const scriptContent = await fetchFromOrigin(path);
const scriptHash = calculateHash(scriptContent);
const hashes = await loadKnownHashes();
const scriptName = path.split('/').pop();
const expectedHash = hashes[scriptName];
console.log(`Serving ${path} with hash ${scriptHash}`);
if (expectedHash && expectedHash !== scriptHash) {
await sendTamperAlert(path, expectedHash, scriptHash);
}
return new Response(scriptContent, { headers: { 'Content-Type': 'application/javascript' } });
});
| What | What It Does |
|---|---|
| Match .js | This handler only runs for files ending in .js (your payment scripts). |
| Fetch Script | Downloads the script directly from origin (S3). |
| Hash Script | Calculates SHA-384 hash of the script content. |
| Compare Hash | Compares to known-good hash from SSM Parameter Store. |
| Tamper Alert | If the hash doesn’t match, send a SNS alert immediately. |
| Serve Script | If no tamper detected, the script is served back to the user. |
Export Hono App
export default app;
| What | What It Does |
|---|---|
| Expose App | Makes sure this works with Lambda@Edge’s expected handler signature. |