Add Proxmox ISO builder, CSRF protection, boot-files integration

- Add build-proxmox-iso.sh: remaster Ubuntu ISO with autoinstall config,
  offline packages, playbook, webapp, and boot files for zero-touch
  Proxmox VM deployment
- Add boot-files/ directory for WinPE boot files (wimboot, boot.wim,
  BCD, ipxe.efi, etc.) sourced from WestJeff playbook
- Update build-usb.sh and test-vm.sh to bundle boot-files automatically
- Add usb_root variable to playbook, fix all file copy paths to use it
- Unify Apache VirtualHost config (merge default site + webapp proxy)
- Add CSRF token protection to all webapp POST forms and API endpoints
- Update README with Proxmox deployment instructions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-09 20:01:19 -05:00
parent cb442f971b
commit f3a384fa1a
14 changed files with 492 additions and 32 deletions

View File

@@ -3,6 +3,7 @@
import logging
import os
import secrets
import shutil
import subprocess
import tempfile
@@ -11,12 +12,14 @@ from pathlib import Path
from flask import (
Flask,
abort,
flash,
jsonify,
redirect,
render_template,
request,
send_file,
session,
url_for,
)
from lxml import etree
@@ -71,6 +74,32 @@ FRIENDLY_NAMES = {
"ge-shopfloor-mce": "GE Legacy Shop Floor MCE",
}
# ---------------------------------------------------------------------------
# CSRF protection
# ---------------------------------------------------------------------------
def generate_csrf_token():
"""Return the CSRF token for the current session, creating one if needed."""
if "_csrf_token" not in session:
session["_csrf_token"] = secrets.token_hex(32)
return session["_csrf_token"]
@app.context_processor
def inject_csrf_token():
"""Make csrf_token() available in all templates."""
return {"csrf_token": generate_csrf_token}
@app.before_request
def validate_csrf():
"""Reject POST requests with a missing or invalid CSRF token."""
if request.method != "POST":
return
token = request.form.get("_csrf_token") or request.headers.get("X-CSRF-Token")
if not token or token != generate_csrf_token():
abort(403)
NS = "urn:schemas-microsoft-com:unattend"
WCM = "http://schemas.microsoft.com/WMIConfig/2002/State"
NSMAP = {None: NS, "wcm": WCM}

View File

@@ -243,9 +243,13 @@ document.addEventListener('DOMContentLoaded', function () {
saveRawBtn.disabled = true;
saveRawBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Saving...';
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({ raw_xml: xmlContent })
})
.then(function (resp) { return resp.json(); })

View File

@@ -72,6 +72,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ url_for('clonezilla_upload') }}" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-upload me-2"></i>Upload Backup</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
@@ -100,6 +101,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}PXE Server Manager{% endblock %}</title>
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">

View File

@@ -13,6 +13,7 @@
<div class="card-body">
{% if usb_mounts %}
<form method="POST" id="importForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="source" class="form-label fw-semibold">Source (USB Mount Point)</label>
<select class="form-select" name="source" id="source" required>

View File

@@ -79,6 +79,7 @@
<div class="modal-dialog">
<div class="modal-content">
<form id="deleteForm" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle me-2 text-danger"></i>Confirm Delete</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>

View File

@@ -52,6 +52,7 @@
</div>
<div class="card-body p-0">
<form action="{{ url_for('startnet_save') }}" method="post" id="startnetForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<textarea name="content" class="form-control cmd-editor" id="cmdEditor"
spellcheck="false">{{ content }}</textarea>
</form>

View File

@@ -70,6 +70,7 @@
</ul>
<form method="POST" id="unattendForm">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="tab-content">
<!-- ==================== FORM VIEW ==================== -->