Przeglądaj źródła

Fix ops panel CSP login under Cloudflare

Maciek 3 tygodni temu
rodzic
commit
487e7da703
3 zmienionych plików z 15 dodań i 7 usunięć
  1. 10 6
      public/ops.html
  2. 1 1
      src/http/routes/ops.ts
  3. 4 0
      tests/integration/opsPanel.test.ts

+ 10 - 6
public/ops.html

@@ -110,10 +110,10 @@
   <div class="box">
     <h2>EKSRelay — panel operacyjny</h2>
     <p>Podgląd tylko do odczytu. Podaj <code>ADMIN_TOKEN</code>, żeby wczytać dane.</p>
-    <form id="gate-form" autocomplete="off">
+    <div id="gate-form" role="group" aria-label="Logowanie do panelu" autocomplete="off">
       <input type="password" id="token-input" placeholder="ADMIN_TOKEN" autocomplete="off" spellcheck="false" required>
-      <button type="submit" class="primary">Zaloguj</button>
-    </form>
+      <button type="button" id="login-button" class="primary">Zaloguj</button>
+    </div>
     <div class="err" id="gate-err"></div>
     <div class="hint">
       Token trzymany jest wyłącznie w <code>sessionStorage</code> tej karty i wysyłany
@@ -178,7 +178,7 @@
   </main>
 </div>
 
-<script>
+<script data-cfasync="false">
 (function () {
   'use strict';
 
@@ -508,8 +508,7 @@
 
   // ------------------------------------------------------------------- wiring
 
-  $('gate-form').addEventListener('submit', function (e) {
-    e.preventDefault();
+  function submitLogin() {
     var value = $('token-input').value.trim();
     if (!value) return;
     sessionStorage.setItem(KEY, value);
@@ -522,6 +521,11 @@
         $('gate-err').style.display = 'block';
       }
     });
+  }
+
+  $('login-button').addEventListener('click', submitLogin);
+  $('token-input').addEventListener('keydown', function (e) {
+    if (e.key === 'Enter') { e.preventDefault(); submitLogin(); }
   });
 
   $('tabs').addEventListener('click', function (e) {

+ 1 - 1
src/http/routes/ops.ts

@@ -31,7 +31,7 @@ opsRouter.get('/ops', (_req, res, next) => {
   // future edit cannot accidentally ship the token to a third party.
   res.setHeader(
     'Content-Security-Policy',
-    "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
+    "default-src 'none'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; script-src-elem 'self' 'unsafe-inline' https://challenges.cloudflare.com; style-src 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'",
   );
   res.sendFile(OPS_HTML, (err) => {
     if (err) next(err);

+ 4 - 0
tests/integration/opsPanel.test.ts

@@ -91,8 +91,12 @@ test('the shell sends hardening headers', async () => {
   assert.match(res.headers.get('x-robots-tag') ?? '', /noindex/);
   const csp = res.headers.get('content-security-policy') ?? '';
   assert.match(csp, /default-src 'none'/);
+  assert.match(csp, /script-src-elem/);
+  assert.match(csp, /img-src 'self'/);
   assert.match(csp, /connect-src 'self'/);
   assert.match(csp, /frame-ancestors 'none'/);
+  assert.match(res.text, /data-cfasync="false"/);
+  assert.ok(!res.text.includes('<form'), 'login must not depend on a form submit that CSP/Cloudflare can block');
 });
 
 test('the panel is not mounted inside the protected /admin namespace', async () => {