Add CSP, SRI, and auto-hash deploy pipeline
- Content Security Policy via <meta> tag (blocks exfiltration to foreign domains) - Subresource Integrity on all static and dynamically loaded scripts - Nginx security headers snippet (HSTS, CSP, frame-ancestors on all responses) - Auto-minify and SRI hash update in deploy.sh (prevents stale hashes)
This commit is contained in:
4
app.js
4
app.js
@@ -614,6 +614,8 @@
|
||||
if (window.jspdf) { resolve(); return; }
|
||||
var script = document.createElement('script');
|
||||
script.src = 'lib/jspdf.min.js';
|
||||
script.integrity = 'sha384-GwHhSt8QjC7J+v0zZ0Flfho/T76YHEcCL9w4rvjTIUHauh6gWJeBSIi3vWXxNhtA';
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.onload = function () { pdfLoaded = true; resolve(); };
|
||||
script.onerror = function () { reject(new Error('Failed to load jsPDF')); };
|
||||
document.head.appendChild(script);
|
||||
@@ -820,6 +822,8 @@
|
||||
if (window.XmrCrypto) { resolve(); return; }
|
||||
const script = document.createElement('script');
|
||||
script.src = 'lib/xmr-crypto.bundle.js';
|
||||
script.integrity = 'sha384-ta9IpDZOod8WcA7TprKyb/TxmOSNfkG0fCjhWssiSmpft9MLXAtSO8L8YmnH3DCY';
|
||||
script.crossOrigin = 'anonymous';
|
||||
script.onload = resolve;
|
||||
script.onerror = function () { reject(new Error('Failed to load crypto module')); };
|
||||
document.head.appendChild(script);
|
||||
|
||||
2
app.min.js
vendored
2
app.min.js
vendored
File diff suppressed because one or more lines are too long
11
index.html
11
index.html
@@ -5,9 +5,10 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>xmrpay.link — Monero Invoice Generator</title>
|
||||
<meta name="description" content="Create Monero payment requests in seconds. No account registration, no KYC. Minimal backend for short URLs only.">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; base-uri 'none'">
|
||||
<link rel="icon" id="favicon" href="favicon.svg" type="image/svg+xml">
|
||||
<link rel="preload" href="fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="style.css?v=20260326-3">
|
||||
<link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-ntklmpjtBHVfhFRH4pdoZb86mpDOt3aBw7s2XFpvGgT8vrelMuw6H4EnPultQEXT" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -114,7 +115,7 @@
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p data-i18n-html="footer">Open Source · No Tracking · No KYC · <a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a></p>
|
||||
<p data-i18n-html="footer">Open Source · No Tracking · No KYC<br /><a href="https://gitea.schmidt.eco/schmidt1024/xmrpay.link" target="_blank" rel="noopener noreferrer">Source</a> · <a href="http://mc6wfeaqc7oijgdcudrr5zsotmwok3jzk3tu2uezzyjisn7nzzjjizyd.onion" title="Tor Hidden Service">Onion</a> · <a href="/privacy.html">Privacy & Terms</a></p>
|
||||
</footer>
|
||||
|
||||
<div class="lang-picker" id="langPicker">
|
||||
@@ -130,8 +131,8 @@
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="lib/qrcode.min.js?v=20260326-3" defer></script>
|
||||
<script src="i18n.min.js?v=20260326-3" defer></script>
|
||||
<script src="app.min.js?v=20260326-3" defer></script>
|
||||
<script src="lib/qrcode.min.js?v=20260326-3" integrity="sha384-3zSEDfvllQohrq0PHL1fOXJuC/jSOO34H46t6UQfobFOmxE5BpjjaIJY5F2/bMnU" crossorigin="anonymous" defer></script>
|
||||
<script src="i18n.min.js?v=20260326-3" integrity="sha384-DOaPisq9d2LQogorLYKjAclE021CXio6vwhHo/hpBNtc3LaITnVtkxqNT2FVrunW" crossorigin="anonymous" defer></script>
|
||||
<script src="app.min.js?v=20260326-3" integrity="sha384-tdgiaUZYJ6E+/EqlbzOvxRvySKQZNdxjNktRV3K75fitMLdkR5DXuVU9XTppNku5" crossorigin="anonymous" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
12
nginx-security-headers.conf
Normal file
12
nginx-security-headers.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
# Include this in your nginx server block:
|
||||
# include /path/to/nginx-security-headers.conf;
|
||||
#
|
||||
# This ensures ALL responses (HTML, CSS, JS, fonts) get security headers,
|
||||
# not just PHP/API responses.
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'" always;
|
||||
@@ -5,8 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>xmrpay.link — Privacy & Terms</title>
|
||||
<meta name="description" content="Privacy policy and terms of use for xmrpay.link.">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; base-uri 'none'">
|
||||
<link rel="icon" href="favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="style.css?v=20260326-3">
|
||||
<link rel="stylesheet" href="style.css?v=20260326-3" integrity="sha384-ntklmpjtBHVfhFRH4pdoZb86mpDOt3aBw7s2XFpvGgT8vrelMuw6H4EnPultQEXT" crossorigin="anonymous">
|
||||
<style>
|
||||
main.legal-main {
|
||||
max-width: 920px;
|
||||
|
||||
@@ -68,6 +68,52 @@ else
|
||||
echo "Skipping pre-deploy backup (DEPLOY_BACKUP_ENABLE=0)."
|
||||
fi
|
||||
|
||||
# ── Minify & update SRI hashes ────────────────────────────────────────────────
|
||||
echo "Minifying JS..."
|
||||
TERSER="${TERSER:-terser}"
|
||||
if ! command -v "$TERSER" &>/dev/null; then
|
||||
echo "Error: terser not found. Install with: npm i -g terser" >&2
|
||||
exit 1
|
||||
fi
|
||||
"$TERSER" app.js -c -m -o app.min.js
|
||||
"$TERSER" i18n.js -c -m -o i18n.min.js
|
||||
|
||||
echo "Updating SRI hashes..."
|
||||
sri_hash() { echo "sha384-$(openssl dgst -sha384 -binary "$1" | openssl base64 -A)"; }
|
||||
|
||||
HASH_STYLE=$(sri_hash style.css)
|
||||
HASH_QRCODE=$(sri_hash lib/qrcode.min.js)
|
||||
HASH_I18N=$(sri_hash i18n.min.js)
|
||||
HASH_APP=$(sri_hash app.min.js)
|
||||
HASH_JSPDF=$(sri_hash lib/jspdf.min.js)
|
||||
HASH_CRYPTO=$(sri_hash lib/xmr-crypto.bundle.js)
|
||||
|
||||
# Update index.html SRI attributes
|
||||
sed -i -E \
|
||||
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_STYLE}|" \
|
||||
-e "s|(qrcode\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_QRCODE}|" \
|
||||
-e "s|(i18n\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_I18N}|" \
|
||||
-e "s|(app\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_APP}|" \
|
||||
index.html
|
||||
|
||||
# Update privacy.html SRI attributes
|
||||
sed -i -E \
|
||||
-e "s|(style\.css[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_STYLE}|" \
|
||||
privacy.html
|
||||
|
||||
# Update dynamic SRI hashes in app.js and re-minify if changed
|
||||
sed -i -E \
|
||||
-e "s|(jspdf\.min\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${HASH_JSPDF}|" \
|
||||
-e "s|(xmr-crypto\.bundle\.js.*integrity\s*=\s*')sha384-[A-Za-z0-9+/=]+|\1${HASH_CRYPTO}|" \
|
||||
app.js
|
||||
|
||||
# Re-minify app.js (dynamic SRI hashes may have changed)
|
||||
"$TERSER" app.js -c -m -o app.min.js
|
||||
HASH_APP_FINAL=$(sri_hash app.min.js)
|
||||
sed -i -E "s|(app\.min\.js[^\"]*\"\s+integrity=\")sha384-[A-Za-z0-9+/=]+|\1${HASH_APP_FINAL}|" index.html
|
||||
|
||||
echo "SRI hashes updated."
|
||||
|
||||
RSYNC_DRY_RUN=""
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
RSYNC_DRY_RUN="--dry-run"
|
||||
|
||||
Reference in New Issue
Block a user