Report on security risks of exposing a tunneled webapp
Date: 2025 06-June 27
Clayton 0:
Shared index html for https://api.pavlov3d.world
AI 0:
Thanks for sharing your index HTML! This helps identify the attack surface exposed by your live plotting frontend. Here’s a focused rundown of what info in this page and its behavior could potentially be leveraged maliciously:
What Info Could Be Leveraged Maliciously From This Frontend?
1. The /data
Endpoint
-
The script fetches live data from
/data
every 2 seconds. -
If
/data
has no authentication or rate limiting, attackers could:-
Harvest raw data continuously.
-
Scrape potentially sensitive or proprietary data.
-
Perform denial-of-service (DoS) by hammering the endpoint with requests.
-
Discover the shape and types of data your API provides, enabling targeted attacks.
-
2. Data Structure & Labels
-
You reveal that the response is JSON with labels and x/y series arrays.
-
If labels or data represent internal sensor names, plant locations, or system states, an attacker can gain reconnaissance on:
-
What your system monitors.
-
Your naming conventions.
-
Timing/frequency of data updates.
-
-
This info could facilitate social engineering or direct attacks on your infrastructure.
3. Frontend Exposure
-
The HTML references Plotly CDN, which is generally safe but depends on trusting third-party scripts.
-
If your app accepts user input without sanitization (not shown here), XSS attacks are possible.
-
The script creates new plots every 2 seconds, which may open a DoS vector if not throttled on server side.
4. No Authentication or Authorization Visible
-
The page does not indicate any login or token-based access.
-
If your server also lacks auth on
/data
or this page, it’s fully public. -
Public endpoints expose your system to automated scanning and brute force.
What To Do To Harden This
-
Require Authentication & Authorization on both the web page and
/data
endpoint.- Use tokens, session cookies, or Cloudflare Access integration.
-
Implement rate limiting on
/data
to avoid abuse. -
Ensure CORS policies are tight, only allowing your domain.
-
If data is sensitive, consider data obfuscation or anonymization before exposing.
-
Log and monitor access to
/data
and the web app. -
Consider serving the index page only after auth to avoid leaking data endpoint URL.
-
Sanitize all data that might be displayed or accepted.
-
Regularly review 3rd-party CDN scripts for vulnerabilities or use a local copy you control.
Summary
The biggest concern is that anyone with the URL can access /data
and the live data feed freely, scraping or abusing it.
By itself, the HTML isn’t revealing passwords or secrets, but combined with your backend config and access control (or lack thereof), it can be leveraged for:
-
Data exfiltration
-
Reconnaissance of your monitoring system
-
Automated attacks if abused heavily
If you want, I can help draft a minimal Flask or FastAPI auth layer you could plug in to protect the /data
endpoint and the web app? Or help configure Cloudflare Access or API gateway policies?
Clayton 1:
I am most interested in
-
Implement rate limiting on
/data
to avoid abuse. -
Ensure CORS policies are tight, only allowing your domain.
Talk about the theory. if you share snippets, use fastapi.
AI 1:
Got it! Let’s dive into the theory behind rate limiting and CORS policies, then I'll show you how those map into FastAPI with minimal example snippets.
1. Rate Limiting Theory
What is Rate Limiting?
Rate limiting controls how many requests a client (identified usually by IP or API key) can make to your API in a given time frame (e.g., 100 requests per minute). This protects your backend from:
-
Abuse or scraping: Prevents attackers from flooding your
/data
endpoint with requests to steal data or degrade performance. -
Denial of Service (DoS): Mitigates attacks aimed to overwhelm resources.
-
Fair usage: Ensures all clients get fair access to the resource.
How Does It Work?
-
The server counts incoming requests per client.
-
If the count exceeds the limit, the server rejects requests with an HTTP 429 "Too Many Requests" status.
-
Limits reset after the time window.
Common Implementations
-
Fixed window (e.g., 100 req/min)
-
Sliding window (more granular)
-
Token bucket or leaky bucket algorithms (more smooth rate limiting)
2. CORS Policy Theory
What is CORS?
Cross-Origin Resource Sharing (CORS) is a security feature in browsers that restricts web pages from making requests to a different domain than the one serving the page — unless the server explicitly allows it.
Why is CORS Important Here?
Your frontend is fetching /data
— if the web app is served from domain A and the API is on domain B (or a different port), browsers will block the requests unless CORS is properly configured.
How to Secure CORS?
-
Only allow origins (domains) you trust (your frontend domain).
-
Don’t use wildcard (
*
) in production. -
Restrict allowed methods (GET, POST, etc.).
-
Optionally restrict headers allowed.
FastAPI Implementation Examples
a) Rate Limiting with FastAPI
FastAPI does not have built-in rate limiting, but you can use third-party libraries like slowapi
(built on limits
) or implement a simple in-memory rate limiter.
Here’s a minimal example using slowapi:
from fastapi import FastAPI, Request, HTTPException
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
app = FastAPI()
# Initialize limiter: 5 requests per minute per IP
limiter = Limiter(key_func=get_remote_address, default_limits=["5/minute"])
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/data")
@limiter.limit("10/minute") # Specific rate limit on this route
async def get_data(request: Request):
# Your data retrieval logic here
return {"x": [1, 2, 3], "y": [4, 5, 6]}
-
The decorator
@limiter.limit("10/minute")
limits to 10 requests per minute per client IP. -
Clients exceeding this get HTTP 429 automatically.
b) CORS Configuration with FastAPI
FastAPI uses fastapi.middleware.cors.CORSMiddleware
.
Example allowing only your domain:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"https://yourdomain.com", # Replace with your actual frontend domain
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # Only allow your frontend domain
allow_credentials=True,
allow_methods=["GET", "POST"], # Restrict to only needed methods
allow_headers=["*"], # Or specify specific headers if needed
)
@app.get("/data")
async def get_data():
# Your API logic here
return {"x": [1, 2, 3], "y": [4, 5, 6]}
-
Requests from any other origin than
"https://yourdomain.com"
will be blocked by browsers. -
This does not prevent direct API calls from curl/postman, but blocks browser-based cross-site requests.
Summary
Feature | Purpose | FastAPI Implementation |
---|---|---|
Rate limiting | Protect /data from excessive calls |
Use slowapi or custom logic with decorators or middleware |
CORS policy | Restrict frontend access to trusted origins | Use CORSMiddleware to whitelist your frontend domain |
If you want, I can help you craft a minimal FastAPI app with both these protections integrated, ready to plug into your existing backend.
Clayton 2:
right now cloudflared is tunneling to a public subdomain. can i instead tunnel to a priviate subdomain set up on an AWS server, and then from there tunnel to he public subdomain?
The purpose of this: the initial server needs to be secure. It is on the network necessary to access the source of the data. I need to pass the data, via API or tunneling, to a system that is accesible.
This was, the initial in-house server doesn't need to see all of the traffic for the public subdomain when people visit the web app, it is only passing the data feed to the system that will host the public web app and see the traffic
AI 2:
Yes — what you're describing is a proxy or relay architecture, where:
-
Your internal in-house server (secure zone) only needs to send data.
-
Your intermediate AWS server receives the data and hosts the web app publicly.
-
The public Cloudflare tunnel points only to this AWS instance.
This is a good practice for isolating secure infrastructure. Let's break it down and discuss your architecture and how to implement it.
🔐 Problem Context
You want:
-
Internal server (e.g., on City of Memphis network) → secure, not public.
-
Public server (e.g., AWS EC2) → can serve
/data
and host frontend. -
Web users should only connect to AWS.
-
Internal server pushes or tunnels just the live data to AWS.
-
Internal server does not serve public web traffic or host Cloudflare directly.
✅ Architecture Overview
[SECURE INTERNAL NETWORK]
┌────────────────────────┐
│ Internal Server │
│ - Access to raw EDS │
│ - FastAPI or script │
│ - Sends JSON feed │
└────────┬───────────────┘
│ (pull or push)
▼
[DMZ or AWS SERVER]
┌────────────────────────┐
│ AWS EC2 │
│ - Receives data │
│ - Hosts live web app │
│ - Has public Cloudflare|
│ tunnel to subdomain │
└────────┬───────────────┘
│
▼
[CLIENT DEVICES]
- Access app via
`https://your-tunnel.cloudflare.com`
🔧 Implementation Options
✅ Option 1: Push from Internal to AWS
Internal Server:
- Periodically send
POST
requests tohttps://aws-server.internal/api/feed
(use internal tunnel or IPsec/VPN if required).
AWS Server:
-
Expose an endpoint (
POST /api/feed
) to receive and cache the data (e.g., in memory). -
Serve
/data
publicly through the Cloudflare tunnel.
➡️ Pros:
-
Simpler.
-
Stateless.
-
Easy to scale.
➡️ Cons:
- Internal server needs to initiate connections (cannot be fully passive).
✅ Option 2: Cloudflare Tunnel from Internal to AWS
You can run a Cloudflare Tunnel from the internal server to AWS, allowing the AWS instance to fetch()
data from the internal system via its private tunnel address.
This is zero trust style:
-
Internal server does:
cloudflared tunnel run internal-data-feed
-
AWS server fetches data from:
https://internal-data-feed.example.cloudflareaccess.com/data
-
This URL is never public — it's protected by Cloudflare Access and only the AWS server can call it (use mTLS or signed JWT).
➡️ Pros:
-
Your internal server never exposes any service to the internet.
-
Data always flows outward.
➡️ Cons:
- More complex to set up with mTLS or Cloudflare Access rules.
🔐 Security Benefits
-
Internal server is isolated and not exposed to the public web.
-
Web traffic load is handled by AWS (cheap, scalable).
-
Cloudflare tunnel on AWS reduces inbound exposure (no open ports).
-
Only the data channel is exposed, either as a tunnel or outbound POST.
⚙️ FastAPI Example (AWS Server)
# AWS Server: receives data
from fastapi import FastAPI, Request
app = FastAPI()
latest_data = {}
@app.post("/api/feed")
async def receive_feed(request: Request):
global latest_data
latest_data = await request.json()
return {"status": "ok"}
@app.get("/data")
async def serve_data():
return latest_data
Internal server script (pusher):
import requests
import time
def push_data():
while True:
data = get_fresh_data_somehow()
requests.post("https://aws-your-domain/api/feed", json=data)
time.sleep(2)
push_data()
🧱 Recommendation
If you want minimal moving parts and the internal server is allowed to make outbound HTTPS:
-
Go with Option 1 (push API) to AWS.
-
Cloudflare tunnel only exists on the AWS host.
-
Rate limit and authenticate the feed endpoint (
/api/feed
).
If you want true zero trust, or can't push but can tunnel out:
- Use Option 2 (Cloudflare-to-Cloudflare tunnel) with Cloudflare Access between internal and AWS.
Want help setting up either route (e.g., using cloudflared access
, FastAPI relay, or adding authentication)?