diff --git a/.github/workflows/fixture-integrity.yml b/.github/workflows/fixture-integrity.yml new file mode 100644 index 0000000..1b8654b --- /dev/null +++ b/.github/workflows/fixture-integrity.yml @@ -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 diff --git a/README.md b/README.md index 4b27e9b..ac4c29c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crm_safe_simulato.html b/crm_safe_simulato.html new file mode 100644 index 0000000..7884c47 --- /dev/null +++ b/crm_safe_simulato.html @@ -0,0 +1,29 @@ + + + + + + Chimera — Safe CRM Control Fixture + + + +

CRM control — safe rendering

+

Negative control for Chimera's deliberately vulnerable CRM fixture. This page keeps pasted input inert as text.

+ + + +
I dati salvati appariranno qui come testo.
+ + + diff --git a/tests/test_fixture_contracts.py b/tests/test_fixture_contracts.py new file mode 100644 index 0000000..562111c --- /dev/null +++ b/tests/test_fixture_contracts.py @@ -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()