Featured image of post Security Settings for Using CloudFlare R2 as an Image Host to Prevent Malicious Class B File Requests

Security Settings for Using CloudFlare R2 as an Image Host to Prevent Malicious Class B File Requests

CloudFlare R2 is a groundbreaking free service generously offered by the good folks at Cloudflare, providing users with 10GB of storage and unlimited bandwidth by default. However, this “unlimited” comes with some restrictions: no more than 1 million write operations and 10 million read operations per month, beyond which usage will be billed accordingly. Recently, I migrated over 110,000 images to R2 and became concerned about exceeding these limits, so I implemented strict security measures.

Protection Principles

The R2 bucket I needed to secure is part of the soomal.cc website mentioned in the article Migrating Soomal.cc to Hugo.

The website is already hosted on Cloudflare Pages, which provides free hosting. The focus here is securing the images referenced in the HTML.

  1. Restrict direct access to the R2 bucket. Prevent direct access to the R2 bucket via URLs, allowing access only through Cloudflare Workers.

  2. Restrict direct access to image URLs. Block direct requests to image URLs like https://images.soomal.cc/test.webp, ensuring all image requests are made within the context of the original website.

  3. Implement appropriate security policies on the origin site. Since images themselves aren’t suitable for overly restrictive rules, the main approach is to increase the difficulty of directly requesting image links by enforcing security measures on the origin site.


Configuration Steps

Disable R2 Public Access

In the R2 settings, avoid configuring custom domain access and do not expose R2 publicly on the web.

For CORS policies, restrict access to the origin site only.

R2 Settings

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
[  
  {  
    "AllowedOrigins": [  
      "https://soomal.cc",  
      "https://www.soomal.cc"  
    ],  
    "AllowedMethods": [  
      "GET",  
      "HEAD"  
    ],  
    "AllowedHeaders": [  
      "*"  
    ],  
    "ExposeHeaders": [  
      "ETag"  
    ],  
    "MaxAgeSeconds": 3600  
  }  
]  

Add Workers Access Rules

  1. Create a Worker.

Create Worker

  1. Bind the R2 bucket.

Bind Bucket

  1. Add a custom domain and route.

Add Route

  1. Add Worker script.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
addEventListener('fetch', event => {  
  event.respondWith(handleRequest(event.request));  
});  

async function handleRequest(request) {  
  const url = new URL(request.url);  
  const referer = request.headers.get('referer');  
  const allowedDomains = ['soomal.cc', 'www.soomal.cc'];  

  // Handle image requests for images.soomal.cc  
  if (url.host === 'images.soomal.cc') {  
    if (referer && allowedDomains.some(domain => referer.includes(domain))) {  
      const filePath = url.pathname.replace(/^\//, '');  
      try {  
        const object = await R2.get(filePath);  
        if (object === null) {  
          console.log(`File not found: ${filePath}`);  
          return new Response('File Not Found', {  
            status: 404,  
            headers: { 'Content-Type': 'text/plain' }  
          });  
        }  
        const headers = new Headers();  
        headers.set('Content-Type', object.httpMetadata.contentType || 'image/webp');  
        headers.set('Cache-Control', 'public, max-age=31536000');  
        headers.set('Access-Control-Allow-Origin', 'https://soomal.cc');  
        return new Response(object.body, { status: 200, headers });  
      } catch (error) {  
        console.log(`Error: ${error.message}`);  
        return new Response('Internal Server Error', {  
          status: 500,  
          headers: { 'Content-Type': 'text/plain' }  
        });  
      }  
    }  

    console.log(`Unauthorized: Referer ${referer} not allowed`);  
    return new Response('Unauthorized Access', {  
      status: 403,  
      headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'no-store, no-cache' }  
    });  
  }  

  // Return 404 for requests not targeting images.soomal.cc or www.soomal.cc  
  return new Response('Not Found', {  
    status: 404,  
    headers: { 'Content-Type': 'text/plain' }  
  });  
}  

This script ensures that all requests to images.soomal.cc are routed through Workers.

By leveraging Workers’ daily limit of 100,000 requests (3 million per month), it prevents excessive billing for R2 bucket access.

At this point, my goal is achieved. With 100,000 daily requests, this backup site has more than enough capacity. If the limit is exceeded, Workers will simply stop functioning, rendering the images inaccessible.

Add Additional Security Policies (Optional)

With the above settings, all requests to the R2 bucket must originate from soomal.cc.

This allows further security enhancements on the origin site to indirectly protect the images.

  1. Enable Strict SSL Mode.

Enable Strict SSL

  1. Enable Caching. Cache as much as possible.

Caching Rules

  1. Security Rules. Enable features like continuous script monitoring, browser integrity checks, rate limiting, and bot attack mitigation for added protection.

Basic Rate Limiting

Enable Managed Challenge for Overseas Access

Built with Hugo, Powered by Github.
Total Posts: 346, Total Words: 477062.
本站已加入BLOGS·CN