<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CivicMeshFlow</title>
    <description>The latest articles on DEV Community by CivicMeshFlow (@civicmeshflow).</description>
    <link>https://dev.to/civicmeshflow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3626305%2F661eebd2-194a-490e-875f-b90ff5b498d6.png</url>
      <title>DEV Community: CivicMeshFlow</title>
      <link>https://dev.to/civicmeshflow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/civicmeshflow"/>
    <language>en</language>
    <item>
      <title>What I Keep Finding When I Scan Small U.S. Municipal Websites (And How To Fix It In Under An Hour)</title>
      <dc:creator>CivicMeshFlow</dc:creator>
      <pubDate>Mon, 24 Nov 2025 01:29:48 +0000</pubDate>
      <link>https://dev.to/civicmeshflow/what-i-keep-finding-when-i-scan-small-us-municipal-websites-and-how-to-fix-it-in-under-an-hour-2n12</link>
      <guid>https://dev.to/civicmeshflow/what-i-keep-finding-when-i-scan-small-us-municipal-websites-and-how-to-fix-it-in-under-an-hour-2n12</guid>
      <description>&lt;p&gt;I’ve spent the last few months poking at public websites that belong to small U.S. towns, school districts and counties. Not as an attacker, but as the guy who built a small open‑source scanner called &lt;strong&gt;CivicMeshFlow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most of these sites sit on old PHP stacks and very tired CMS installs. Nobody gets promoted for "fixing security headers", and budgets are usually eaten by whatever crisis is happening this month.&lt;/p&gt;

&lt;p&gt;Still, these sites handle real people’s data. So I started running systematic scans, keeping notes as I went.&lt;/p&gt;

&lt;p&gt;Very quickly a pattern showed up: different vendors, different designs, but the &lt;strong&gt;same&lt;/strong&gt; security mistakes repeating everywhere.&lt;/p&gt;

&lt;p&gt;This post is basically my field notes. If you’re responsible for a public‑facing municipal site and only have an hour here and there, this should give you a realistic hardening plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I’ve Been Scanning (Nothing Fancy)
&lt;/h2&gt;

&lt;p&gt;CivicMeshFlow is a Laravel + Python thing I run on my own server. I point it at a domain, it follows redirects, inspects response headers, hits a few common paths and then spits out a report with grades and a PDF.&lt;/p&gt;

&lt;p&gt;Even with the tool, I still fall back to the terminal a lot. A typical quick check looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Follow redirects and inspect final headers&lt;/span&gt;
curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; - https://example.gov/ &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,40p'&lt;/span&gt;

&lt;span class="c"&gt;# Make sure HTTP is strictly redirected to HTTPS&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; http://example.gov/ | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'1,20p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If Cloudflare is in front, I hit both the apex and &lt;code&gt;www&lt;/code&gt;, and I always try plain &lt;code&gt;http://&lt;/code&gt; just to see how that very first hop behaves.&lt;/p&gt;

&lt;p&gt;On one of the first counties I tested, the naked &lt;code&gt;http://&lt;/code&gt; version quietly served an old login form over HTTP with no redirect. From the outside it looked like a normal site; from the inside it was a MITM buffet.&lt;/p&gt;

&lt;p&gt;I’m not doing anything you’d call a penetration test. The goal here is humbler: &lt;strong&gt;don’t leave the obviously‑bad stuff online for years.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1. HTTPS Is There… But HSTS Is Nowhere
&lt;/h2&gt;

&lt;p&gt;Almost every site I looked at &lt;em&gt;does&lt;/em&gt; redirect to HTTPS. That’s the good news.&lt;/p&gt;

&lt;p&gt;The bad news: a surprising number never send a &lt;code&gt;Strict-Transport-Security&lt;/code&gt; header. That means the first visit can still be downgraded to HTTP by anyone sitting on the same Wi‑Fi.&lt;/p&gt;

&lt;p&gt;If you’re on Nginx, the fix is literally one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;preload"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Apache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="nc"&gt;Header&lt;/span&gt; &lt;span class="ss"&gt;always&lt;/span&gt; &lt;span class="ss"&gt;set&lt;/span&gt; Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If flipping that switch makes you nervous, start with a smaller value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=86400"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let it run for a day, watch your logs, then move to a full year and add &lt;code&gt;includeSubDomains; preload&lt;/code&gt; when you’re confident.&lt;/p&gt;

&lt;p&gt;On my first batch of ~30 municipal sites, only a small handful had HSTS configured correctly end‑to‑end. Everybody else just hoped the redirect would be enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. No CSP At All (Or Something That Might As Well Be None)
&lt;/h2&gt;

&lt;p&gt;The next recurring issue is Content Security Policy, or rather the lack of it.&lt;/p&gt;

&lt;p&gt;These sites usually have a long history: different contractors, different plugins, a few widgets that nobody remembers adding. The end result is a soup of inline scripts and external JavaScript from half the internet.&lt;/p&gt;

&lt;p&gt;Without a CSP, one compromised third‑party script can silently hijack the whole page for every resident who visits it. That’s a pretty big risk for a town hall announcing tax deadlines or school portals.&lt;/p&gt;

&lt;p&gt;Here’s a conservative starter policy that already shuts a lot of doors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://www.google-analytics.com;
  style-src  'self' 'unsafe-inline';
  img-src    'self' data:;
  connect-src 'self';
  frame-ancestors 'none';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I usually recommend in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ship this as &lt;strong&gt;Report-Only&lt;/strong&gt; first.&lt;/li&gt;
&lt;li&gt;Let it run for a week while you watch the console and logs.&lt;/li&gt;
&lt;li&gt;Add hashes or nonces for the inline scripts that truly need to stay.&lt;/li&gt;
&lt;li&gt;Then flip it into enforcing mode.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s not glamorous work, but the difference between "no CSP" and "baseline CSP" is huge.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Leaky Files and Debug Leftovers
&lt;/h2&gt;

&lt;p&gt;This one still surprises me, but I see it enough that CivicMeshFlow has special checks just for it.&lt;/p&gt;

&lt;p&gt;Things I’ve found on public municipal domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt; files readable straight from the browser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.git/&lt;/code&gt; directories with commit history&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;phpinfo.php&lt;/code&gt; on test subdomains that were never actually decommissioned&lt;/li&gt;
&lt;li&gt;editor backup files, &lt;code&gt;old/&lt;/code&gt; directories, and the classic &lt;code&gt;backup.zip&lt;/code&gt; sitting in web root&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of this can be fixed with two short snippets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nginx:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(git|svn|hg|env)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;/phpinfo.php&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Apache:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight apache"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="sr"&gt; "^(\.git|\.env|phpinfo\.php)$"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="nc"&gt;Require&lt;/span&gt; &lt;span class="ss"&gt;all&lt;/span&gt; denied
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nl"&gt;FilesMatch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you’re behind Cloudflare anyway, use it as an extra safety net:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Field: &lt;code&gt;URI Path&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Operator: &lt;code&gt;contains&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Action: &lt;code&gt;Block&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do similar rules for &lt;code&gt;/backup.zip&lt;/code&gt; and other “temporary” artifacts that, in reality, live forever.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Session Cookies That Trust Everyone
&lt;/h2&gt;

&lt;p&gt;Another pattern that keeps showing up: session cookies without &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;HttpOnly&lt;/code&gt; or &lt;code&gt;SameSite&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On PHP you can centralize this change and not hunt through every call to &lt;code&gt;setcookie&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;session_set_cookie_params&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="s1"&gt;'secure'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'httponly'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'samesite'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Lax'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or 'Strict' if you never rely on cross-site flows&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you’re there, add the usual defensive headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer-when-downgrade
Permissions-Policy: geolocation=(), microphone=(), camera=()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this will make headlines, but it removes a lot of trivial angles for abuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Client Libraries From Another Era
&lt;/h2&gt;

&lt;p&gt;The last recurring theme is JavaScript and CSS from a very different time.&lt;/p&gt;

&lt;p&gt;It’s common to see jQuery 1.x or 2.x, very old Bootstrap, and plugin bundles that haven’t been touched in a decade. I get why: upgrading feels risky, nobody wants to be the person who broke the public website before a council meeting.&lt;/p&gt;

&lt;p&gt;My rule of thumb when I talk to local IT folks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade the &lt;strong&gt;core library&lt;/strong&gt; first, to a maintained version.&lt;/li&gt;
&lt;li&gt;Keep the scary plugins isolated and behind extra testing.&lt;/li&gt;
&lt;li&gt;Do a small, controlled maintenance window and be ready to roll back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve broken layouts by jumping two Bootstrap majors in one go. It wasn’t fun. But staying on vulnerable versions forever isn’t great either, especially for sites that citizens rely on daily.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Realistic 60‑Minute Hardening Plan
&lt;/h2&gt;

&lt;p&gt;If you only have an hour this month to work on a municipal site, here’s how I would spend it.&lt;/p&gt;

&lt;h3&gt;
  
  
  0–10 minutes: HSTS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add HSTS with a small &lt;code&gt;max-age&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test &lt;code&gt;http://&lt;/code&gt; → &lt;code&gt;https://&lt;/code&gt; on apex and &lt;code&gt;www&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Once things look stable, move to a full year and include subdomains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10–25 minutes: CSP
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Deploy a Report‑Only CSP like the starter above.&lt;/li&gt;
&lt;li&gt;Check your logs and the browser console for violations.&lt;/li&gt;
&lt;li&gt;Allow the analytics and scripts that you truly depend on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  25–35 minutes: Kill the obvious leaks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Block &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.git/&lt;/code&gt;, &lt;code&gt;phpinfo.php&lt;/code&gt;, directory listings.&lt;/li&gt;
&lt;li&gt;Verify with &lt;code&gt;curl&lt;/code&gt; that they now respond with 403 or 404.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  35–45 minutes: Cookies and headers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tighten your session cookie settings.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;X-Content-Type-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;Permissions-Policy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  45–60 minutes: Update the worst offenders
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Identify very old jQuery/Bootstrap.&lt;/li&gt;
&lt;li&gt;Replace them with supported versions.&lt;/li&gt;
&lt;li&gt;Smoke‑test login, forms, admin and any public task flows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is your site “unhackable” after this? Of course not. But the bar for exploitation is now higher than “open Wi‑Fi and a browser”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where CivicMeshFlow Helps
&lt;/h2&gt;

&lt;p&gt;This whole checklist grew out of me being tired of doing the same manual checks over and over.&lt;/p&gt;

&lt;p&gt;CivicMeshFlow tries to automate the boring part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It grades your header posture.&lt;/li&gt;
&lt;li&gt;It flags risky endpoints and obvious leak patterns.&lt;/li&gt;
&lt;li&gt;It groups recommendations into &lt;strong&gt;critical&lt;/strong&gt;, &lt;strong&gt;important&lt;/strong&gt; and &lt;strong&gt;optional&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It exports a timestamped PDF you can attach to tickets or internal reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open‑source, and my goal is to keep it free for small public‑sector organizations in the U.S. If that’s you, I’m happy to run a couple of domains and talk through the findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  About Me
&lt;/h2&gt;

&lt;p&gt;I’m &lt;strong&gt;Nick Tkachenko&lt;/strong&gt;, a senior PHP/Laravel developer based in Chicago and the person behind &lt;strong&gt;CivicMeshFlow&lt;/strong&gt;, an open‑source platform for automatic auditing of vulnerable web services used by small U.S. government organizations.&lt;/p&gt;

&lt;p&gt;If you’re curious about the project, you can find more details at: &lt;a href="https://civicmeshflow.com" rel="noopener noreferrer"&gt;https://civicmeshflow.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>webdev</category>
      <category>php</category>
      <category>security</category>
    </item>
  </channel>
</rss>
