Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/fixture-integrity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Chimera fixture integrity

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
offline-fixtures:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Compile Python files without executing receiver
run: python -m py_compile c2_server.py make_svg.py
- name: Parse standalone JavaScript
run: node --check script.js
- name: Check positive and safe-negative fixtures
run: python -m unittest discover -s tests -v
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ A historical project report written during the initial experiment. It contains s

Treat this README and the executable code as the current description of the project.

## Negative control: safe CRM rendering

The repository now includes two destination fixtures with the same input/output IDs:

| Fixture | Rendering | Expected observation |
| --- | --- | --- |
| `crm_scammer_simulato.html` | Deliberately unsafe `innerHTML` sink | The copied HTML-like suffix can become markup under compatible browser conditions. |
| `crm_safe_simulato.html` | `textContent` sink | The same copied value remains inert, visible text. |

This paired design makes it possible to compare a positive case with a concrete safe negative control, instead of treating execution in a deliberately vulnerable fixture as sufficient evidence of a broader exploit.

The safe fixture does not collect cookies or transmit data. The included CI checks file structure, source syntax, and intended sinks **without running any browser payload or receiver**. Those static checks are not a substitute for a controlled browser compatibility matrix.

To use the fixtures manually, keep the experiment on local loopback and compare the output of both CRM pages with the same controlled sample. Do not expose `c2_server.py` to a shared network: its historical implementation binds to all interfaces and runs in debug mode.

## What actually triggers execution

The clipboard is only a carrier.
Expand Down
29 changes: 29 additions & 0 deletions crm_safe_simulato.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chimera — Safe CRM Control Fixture</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1.5rem; }
label, input { display: block; width: 100%; }
input { padding: 0.6rem; margin: 0.5rem 0 1rem; box-sizing: border-box; }
button { padding: 0.6rem 1rem; }
#sinkArea { margin-top: 1.5rem; border: 2px solid #237a57; padding: 1rem; overflow-wrap: anywhere; white-space: pre-wrap; }
</style>
</head>
<body>
<h1>CRM control — safe rendering</h1>
<p>Negative control for Chimera's deliberately vulnerable CRM fixture. This page keeps pasted input inert as text.</p>
<label for="nameInput">Nome</label>
<input type="text" id="nameInput" autocomplete="off">
<button id="saveButton" type="button">SALVA NEL DB (simulazione)</button>
<div id="sinkArea" aria-live="polite">I dati salvati appariranno qui come testo.</div>
<script>
document.getElementById("saveButton").addEventListener("click", function () {
const inputVal = document.getElementById("nameInput").value;
document.getElementById("sinkArea").textContent = "Ultimo dato salvato: " + inputVal;
});
</script>
</body>
</html>
50 changes: 50 additions & 0 deletions tests/test_fixture_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Offline structural fixture checks; browser execution is not tested here."""
from html.parser import HTMLParser
from pathlib import Path
import unittest

ROOT = Path(__file__).resolve().parents[1]


class IdCollector(HTMLParser):
def __init__(self):
super().__init__()
self.ids = set()

def handle_starttag(self, tag, attrs):
props = dict(attrs)
if "id" in props:
self.ids.add(props["id"])


class FixtureContractTests(unittest.TestCase):
def test_source_fixture_contains_copy_event_and_local_receiver(self):
text = (ROOT / "vittima.html").read_text(encoding="utf-8")
self.assertIn("addEventListener('copy'", text)
self.assertIn("clipboardData.setData('text/plain'", text)
self.assertIn("http://127.0.0.1:5000", text)

def test_positive_and_negative_controls_have_same_input_output_ids(self):
for name in ("crm_scammer_simulato.html", "crm_safe_simulato.html"):
parser = IdCollector()
parser.feed((ROOT / name).read_text(encoding="utf-8"))
self.assertTrue({"nameInput", "sinkArea"} <= parser.ids, name)

def test_positive_control_intentionally_uses_unsafe_sink(self):
text = (ROOT / "crm_scammer_simulato.html").read_text(encoding="utf-8")
self.assertIn(".innerHTML =", text)

def test_negative_control_keeps_input_as_text(self):
text = (ROOT / "crm_safe_simulato.html").read_text(encoding="utf-8")
self.assertIn(".textContent =", text)
self.assertNotIn(".innerHTML", text)
self.assertNotIn("document.cookie", text)

def test_readme_restricts_use_to_authorized_labs(self):
text = (ROOT / "README.md").read_text(encoding="utf-8")
self.assertIn("explicit ownership and authorization", text)
self.assertIn("not a browser exploit", text)


if __name__ == "__main__":
unittest.main()
Loading