carlok — zsh — 88×30
Carlo Perassi

$ grep -l security _posts/*.md

#security

15 posts tagged security. Back to the full blog.

home.sh projects.sh blog.sh writing.sh math.sh cv.sh

roundstorm: a per-launch token, four cleared advisories, and a Tauri shell that compiles in CI

roundstorm’s desktop app now generates 32 random bytes on every launch, hands them to the daemon as ROUNDSTORM_TOKEN and injects them into its own page, and the daemon refuses /api and /ws without it — unset, the headless CLI and every script behave exactly as before. It is narrower than “auth”, and SECURITY.md now says so: it stops another process on the loopback socket and another user on a shared machine, and makes ROUNDSTORM_HOST=0.0.0.0 defensible, but not malware running as the user (17618e7).

deck-lovers: the projector disconnect survives cancellation, and the deploy script reads .env

deck-lovers’ /ws disconnect cleanup ran unshielded, so a cancelled task could skip telling the audience the projector had left and leave stale clients behind — Starlette’s TestClient cancels the app task right after sending the disconnect, which made the websocket tests hang about four runs in ten. The cleanup is now wrapped in a shielded anyio cancel scope, two test races are gone, and a regression test cancels the handler the way the client does (3c4e938).

cdclkit: the trail copy stops going quadratic on decision-only searches

cdclkit’s target-phase search copied the whole trail into the target at every new deepest trail. The comment said improvements become rare quickly, which is true once conflicts start and false before: a search that makes many decisions without one improves on every decision, so n decisions copied 1, 2, …, n entries — 65,536 declared variables and one unit clause took 144 s in Python, 131,072 took 4.2 s natively, and each 4× in variables cost about 13–19× (e43a4ac). A counter now records how much of the trail the target already holds, only the new suffix is copied, and the target still ends up byte-for-byte what the full copy produced.

dratify 0.1.7 fixes a soundness hole, and says why PySAT's CaDiCaL proofs fail

dratify 0.1.6 fixed the README’s PySAT example — it loaded formulas with PySAT’s own parser, which stops at the % line every SATLIB file ends with — and closed a release path that let workflow_dispatch publish from a branch, with the pypi and crates environments now restricted to v* tags (108dd50, 5d0bb47). 0.1.7 is the security fix: three checker bugs found while tracing why PySAT’s CaDiCaL proofs do not verify, one of them a soundness hole where the pure-Python checker accepted refutations of satisfiable formulas when a list of steps held a negative literal — reachable through check_proof, though not from cdclkit’s own solver, and text proofs were never affected — alongside an unbounded literal that could size arrays for 10¹¹ variables and two false rejections, both checkers refusing valid proofs in which a lemma arrived unit at the root (61165f9). The PySAT failure itself is not the checker’s: the binding reads every CaDiCaL proof before flushing it, and with the documented workaround all four CaDiCaL versions verify 50 of 50, where glucose and Lingeling already did (2823854).

cdclkit 0.1.4 requires the dratify that checks proofs correctly

cdclkit 0.1.4 moves both halves onto dratify>=0.1.7, whose three fixed checker bugs include two that reach this package: --self-check could reject a valid proof, and the pure-Python checker could accept a refutation through check_proof (d33f84a). Publishing is now tag-only — the environment restricted to v* tags, the workflow_dispatch trigger gone, and the version check failing outright on any other ref — and make smoke, broken since the dratify split, now ties its module list to the source tree, with a packaging CI job that builds the wheel, installs it cleanly and runs it (b7b5426). Python 3.15 rc3 joins the test matrix (including a native-engine leg) and the README example is now executed by a test; the runbook’s recorded Sigstore gap was already covered, since every file on PyPI has carried a PEP 740 attestation since the first Trusted Publishing release.

pickmydegree stops tracking its own generated coverage reports

pickmydegree had been committing the HTML output of its test-coverage run, and CodeQL flagged the Istanbul report’s sorter.js assets as js/xss-through-dom inside that generated output (11e72fc). Those files are test artifacts rather than application code, so coverage/ is now ignored and the committed reports left the tree — roughly 22,800 lines of generated HTML and CSS removed and three added, merged as PR #2. The app source is untouched; only the repository stops carrying a copy of its own coverage run.

pacenotch fingerprints credentials by expiry instead of hashing the token

pacenotch notices that Claude Code has refreshed its OAuth token by fingerprinting the credentials it reads, and that fingerprint used to be a SHA-256 of the access token — which CodeQL flagged as go/weak-sensitive-data-hashing (a63ee1f). The hash never left memory, but hashing the secret was never needed to detect a refresh: the fingerprint is now claudeAiOauth.expiresAt, a value every refresh moves and which is not secret at all, while a PACENOTCH_TOKEN cannot change while the program runs and gets a fixed fingerprint. It is the second CodeQL-driven correction in pacenotch’s credential layer after the recovery path stopped re-reading the token.

deck-lovers: the projector password stops living in the browser cookie

deck-lovers’ /login handler set the proj_auth cookie to the projector password itself, so the plaintext password lived in every presenter’s browser — which CodeQL reports as py/clear-text-storage-sensitive-data. The server now issues a random per-process session token and compares password and cookie in constant time, which also means a server restart invalidates existing projector cookies; two converter test assertions that matched the bare fonts.googleapis.com hostname (py/incomplete-url-substring-sanitization) now check the exact stylesheet URL built by google_fonts_css_url() (fa4dfce).

portcullis: Phase 1 stops accepting any SSH host key

portcullis replaced paramiko’s AutoAddPolicy — which accepts whatever key a host presents — with an explicit trust-on-first-use policy (25da71b). Phase 1 is the first contact with a VM created seconds earlier by the same process, so there is no prior key to compare against and Hetzner does not publish the fingerprint through its API: the new policy accepts that first key once, logs its type and fingerprint, and Phase 2 still connects with that exact key pinned, so a later substitution is detected. The change addresses CodeQL’s py/paramiko-missing-host-key-validation alert, and the test that asserted the blanket policy now asserts the new one. The secret-handling pass of 21 September had already pinned the key Phase 2 connects with; the first contact was still an unconditional accept.

portcullis stops printing its own secrets and pins the host key across phases

portcullis hardened its own handling of the secrets it moves around during provisioning (f41693f): it no longer logs the first and last four characters of HCLOUD_TOKEN, keys/id_rsa is created 0600 from the start rather than being written and then narrowed, and the smtp.env values are shell-quoted before Phase 2 sources them as root — passwords containing $, spaces, quotes or backticks had been mangled or executed, and the file is now chmod 0600 on the VM before credentials are written into it. Phase 1’s SSH host key is pinned and Phase 2 rejects a different one immediately, so a swapped host cannot receive the second phase’s credentials, and preflight validation now runs before any Hetzner resource exists.

A dependency-advisory sweep across the JavaScript, Python and Go repositories

A pass over open dependency advisories bumped the affected packages in every repository that carried them. On the JavaScript side: pickmydegree (vitest, happy-dom, vite, surge), storygen (vitest 4, vite 6, react-router 7), solids-hunter (vitest 4, vite 6.4.3), eclipse-3d-sim, lifechess, agility-trainer and they-live-agent (vitest 4.1.11), platosdf (vitest and its coverage package, 5.0.1), with lockfile refreshes in deck-lovers and express updates in neon-bumper-cars and collective-starship-game.

portcullis: a two-phase hardened Ubuntu VM for Hetzner Cloud

portcullis is a new public Python repository: one command creates a hardened Ubuntu 26.04 VM on Hetzner Cloud, and it drops the gate first — an unprivileged user with key-only SSH on a random high port, root locked, UFW default-deny and sysctl hardening, all inside about 30 seconds and using only what the stock image already ships — then switches the Hetzner firewall to the new port and deletes the temporary API key before the full CIS-style pass (package upgrades, AppArmor, auditd, AIDE, PAM policy, fail2ban, rkhunter, msmtp alerts, Docker and Podman) runs behind both firewalls. verify.sh then runs 58 checks on the finished host. Everything runs inside a Podman container so nothing is installed locally, and teardown deletes a half-provisioned server together with the keys and firewalls it created. The README was rebranded as the repository went public, with a GitHub Actions test workflow on actions/checkout@v5 and a logo.

portcullis: Vim stops stealing the terminal's mouse selection

portcullis hardens a fresh Ubuntu host, and hardening has to leave the machine pleasant to actually use: Ubuntu’s Vim defaults enable xterm mouse reporting, so on a remote terminal Vim captures the selection and ordinary copy/paste stops working. A new Phase 2 section, 1.6a — Terminal editor defaults, now ships /etc/vim/vimrc.local with set mouse= (and a note in the README’s Phase 2 list), leaving selection to the terminal emulator while anyone who wants Vim’s mouse support can opt back in.

kiwifarmit/cra-lab is public: CI/CD pipelines that produce CRA evidence

kiwifarmit/cra-lab is now public — a small lab in the Kiwifarm org that builds the pipeline half of a Cyber Resilience Act story instead of describing it. Pull requests go through Semgrep SAST plus SCA and IaC scanning (Semgrep rather than CodeQL, which needs GitHub Advanced Security on private repos, and it runs entirely on the runner so the code never leaves it); a v* tag generates a CycloneDX SBOM with Syft, hashes it, hands the digest to the SLSA generic generator for provenance, and runs Trivy over the release SBOM at CRITICAL/HIGH before attaching it to the release (pr-security.yml, release-security.yml). A weekly Trivy and Semgrep sweep covers vulnerability, misconfiguration and licence scanning on a timer, and Dependabot keeps the actions the workflows themselves depend on pinned, with each workflow’s comments citing the CRA Annex I Part II clause it answers.

dash: a serverless DMARC aggregate-report parser for Gmail

dash parses DMARC aggregate reports in a serverless setup for Gmail: it extracts and parses incoming reports, enriches the failing sources, and emails a summary of which senders are failing DMARC and why. The repo is public and actively developed.