Bitcoin Forum
May 15, 2026, 08:43:10 AM *
News: Latest Bitcoin Core release: 31.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: « 1 2 3 4 [5]  All
  Print  
Author Topic: Round#2 [CFNE] Omnisee 🚨 Bug Hunt Campaign – Help To Improve & Get Rewarded! 🐞  (Read 697 times)
irfan_pak10
Legendary
*
Offline

Activity: 3724
Merit: 1721


🧙‍♂️ #kycfree


View Profile WWW
May 08, 2026, 07:07:13 PM
 #81

/sitemap.xml lists only 10 latest blocks — site is invisible to search

Sitemap contains 11 <loc> entries: the homepage and the latest 10 blocks. No /address/{x}, no /tx/{x}, no /faq, no older blocks. For a Bitcoin explorer with hundreds of thousands of indexable pages, this means search engines see almost nothing.

Reproducer
Code:
$ curl -s https://omnisee.io/sitemap.xml | grep -c "<loc>"
11
 
$ curl -s https://omnisee.io/sitemap.xml | grep "<loc>" | head -3
<loc>https://omnisee.io/</loc>
<loc>https://omnisee.io/block/948314</loc>
<loc>https://omnisee.io/block/948313</loc>
   …only the latest 10 blocks…

Impact
•   Search engines miss /faq, popular addresses, recent transactions, older blocks.
•   SEO crawl budget is wasted re-discovering the same 10 latest blocks every visit.
•   Discoverability for the Tor mirror, the FAQ, the Asset-AI Detective explanations is zero.

Fix
Generate a paginated sitemap-index: sitemaps/blocks/{i}.xml, sitemaps/addresses/{i}.xml, sitemaps/transactions/{i}.xml. Include /faq and the Tor mirror. Cap each sub-sitemap at 50 000 URLs (the spec maximum).



ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 10, 2026, 01:23:24 PM
 #82

Report Title: Unsanitized external input stored and returned via API

Report Details: Starting with any address (I use the Silk Road one, but it could be your own or Satoshi’s Genesis address)

https://omnisee.io/api/address/1HQ3Go3ggs8pFnXuHVHRytPCq5fGG8Hbhx/scam-check

and querying the API, you get:

Code:
{
  "address": "1HQ3Go3ggs8pFnXuHVHRytPCq5fGG8Hbhx",
  "is_scam": true,
  "details": {
    "is_scam": true,
    "address": "1HQ3Go3ggs8pFnXuHVHRytPCq5fGG8Hbhx",
    "category": "sextortion",
    "risk_level": "high",
    "sources": [
      "chainabuse"
    ],
    "report_count": 5,
    "first_seen": "2026-03-27T20:45:09+00:00",
    "description": "ChainAbuse: {{7*7}} its for the test in bugcrowd",
    "ai_summary": "## Verdict\nSUSPICIOUS — address linked to sextortion scams, but no authoritative sanctions or law enforcement confirmation.\n\n## Key Factors\n- Flagged by multiple community reports (5) on ChainAbuse as a sextortion scam wallet.\n- On-chain activity shows high total received (208,210 BTC) with zero balance, consistent with a disposable collection address.\n- Mixing score indicates some coinjoin usage, possibly to obfuscate flow.\n\n## Activity Pattern\nAddress likely used to receive payments from sextortion email scams. Large volume suggests widespread targeting; funds have been moved out.\n\n## Recommendation\nMONITOR — while not confirmed by OFAC or major analytics firms, the pattern and multiple reports warrant caution. Interaction may expose users to legal or reputational risk."
  }
}

the line
Code:
"description": "ChainAbuse: {{7*7}} its for the test in bugcrowd"

This shows that the Chainabuse data has not been sanitized it would have returned 49. It is the log of a test carried out on Bugcrowd by someone else, and if it were rendered in HTML on the website. If this field is rendered in an HTML context without escaping, it could lead to Stored XSS.

Based on the bolded, it is compelling to state that there is no basis in "it would have returned 49". The reason for this is that   

Code:
{{7*7}}
is Angular/Handlebars syntax, not Python/Jinja2. This means that FastAPI doesn't evaluate it.
What would return 49: Jinja2/Flask template syntax (
Code:
{{7*7}}
in a template context), which is different from what Omnisee uses: FastAPI (Python), which returns
Code:
{{7*7}}
literally as a string

What I'm saying is that
Code:
{{7*7}}
would NOT evaluate to 49 in this situation, bearing in mind that it's just a string that someone else submitted to ChainAbuse.

Also bear in mind that stored XSS is unproven in this case.

This is a good bug find, just needed correcting the incorrect claims.

🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 10, 2026, 04:23:51 PM
 #83

MAJOR   TLSv1.0 and TLSv1.1 accepted on the edge

RFC 8996 (March 2021) deprecates TLSv1.0/1.1. Modern browsers refuse them, but legacy bots and downgrade-attack tooling still negotiate them. PCI-DSS, NIST SP 800-52r2 disallow them.

Reproducer
Code:
$ nmap --script ssl-enum-ciphers -p 443 omnisee.io | grep -E "TLSv1\.[01]"
| TLSv1.0:
| TLSv1.1:


Impact
•   Audit/compliance flag.
•   Legacy bots and TLS downgrade attacks remain possible.

Fix
Cloudflare → SSL/TLS → Edge Certificates → Minimum TLS Version → set to TLS 1.2 (or 1.3).




Report Title: Additional test cases and recommendations for /block/{invalid_format} handling

Report Details: I looked at this one and ran a few more tests. A couple of things are still missing.

First, there are other inputs worth checking. 0 is the genesis block and works fine. But floats like 1.5 or 3.14159 – those should be a straight 400. Right now they might be slipping through. Overflow numbers like 99999999999999999999 and 18446744073709551616 (that's 2^64) are also edge cases worth testing. A valid looking hash that doesn't actually exist should be 404, not something else. And mixed hex like 123abc456 – that's neither a height nor a real hash, so 400 is the right call.

The real problem here is that invalid input is hitting an unhandled exception and bubbling up as a 5xx. That's not great. The code is probably trying to parse whatever comes in without checking format first. So you get a crash on something that should have been rejected early.

The original report didn't mention a few practical things. First, monitoring will fire on every 503. Someone running a quick fuzz will fill your alert dashboard with noise. Those should be filtered out. Second, logging these as ERROR instead of WARN just adds clutter. It's a user typo, not a broken service. Third, rate limiting these paths would make fuzzing a lot harder.

A proper fix needs a regex for 64-char hex hashes, a sane max height (something like 10 million is safe), and a clear path: check height, then hash, then return 400. Upstream errors should never surface as 5xx. Wrap them and return 404.

Also worth noting, this same pattern is probably sitting in /api/block and /api/transaction as well. Would be good to audit all path parameters in one go.

The bottom line is if someone types garbage, you don't return a 5xx. That's the rule to enforce everywhere. Something like this should work pretty well:

Code:
import re

BLOCK_HASH_PATTERN = re.compile(r'^[0-9a-fA-F]{64}$')
MAX_HEIGHT = 10_000_000

@app.get("/block/{input}")
async def get_block(input: str):
    # Try height
    if input.isdigit():
        height = int(input)
        if height < 0 or height > MAX_HEIGHT:
            return {"error": "height out of range"}, 400
        try:
            result = fetch_by_height(height)
            return result if result else {"error": "not found"}, 404
        except:
            return {"error": "not found"}, 404
    
    # Try hash
    if BLOCK_HASH_PATTERN.match(input):
        try:
            result = fetch_by_hash(input)
            return result if result else {"error": "not found"}, 404
        except:
            return {"error": "not found"}, 404
    
    # Invalid format
    return {"error": "invalid block identifier"}, 400


🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 10, 2026, 05:00:39 PM
 #84

MAJOR   /api/transaction/{nonexistent_txid} returns 500 instead of 404

Valid 64-hex-format but nonexistent txids trigger an unhandled backend exception. API returns bare 'Internal Server Error' (text/plain, 21 bytes). The HTML route /tx/{nonexistent} correctly returns 200 with a 'Transaction Not Found' page — handlers diverge.

Reproducer
Code:
$ for tx in 1111...1111 ffff...ffff 0123...cdef 2cca...47ce; do
    curl https://omnisee.io/api/transaction/$tx
  done
[HTTP 500] tx=1111...      → "Internal Server Error"
[HTTP 500] tx=ffff...      → "Internal Server Error"
[HTTP 500] tx=0123...cdef  → "Internal Server Error"
[HTTP 500] tx=2cca...47ce  → "Internal Server Error"

Impact
•   Reveals an unhandled exception path on the API.
•   Wrong status code (500) confuses any client doing 'if 404 then ... else error' control flow.
•   Pollutes error metrics / SRE dashboards with fake outages.

Fix
Wrap the upstream provider call in try/except; return 404 + JSON {"error":"Transaction not found","code":"TX_NOT_FOUND"} when no provider returns the tx.




I tried to reproduce this but kept running into Cloudflare blocking me with 520 errors after just a couple requests. Couldn't get far enough to confirm if the 500 is still there. That said, there's a few things worth adding to this report.

First, the fix mentioned is solid but missing something. The API returns JSON for other errors like invalid format, but right now on 500 it's returning plain text. Whatever fix Omnisee implements should keep the response format consistent, ie JSON across the board, even for 404. Clients expecting JSON will break on plain text.

Second, worth checking how the HTML route
Code:
/tx/{nonexistent}
handles this. The reports says it returns 200 with a not-found page. That's okay for a browser but the API should stick to proper HTTP status codes. I believe that 200 for a missing resource is technically incorrect even if it works for humans.

The third issue is that the difference between API and HTML routes matters for anyone who would like to build tools on top of the Omnisee app. If an API client gets 500 today, they might retry or alert. But a 404 would tell them to stop immediately. The current behavior could cause unnecessary retries and false alarms.

One more thing, I suggest if someone can still test this, try edge cases like 63-character hex, 65-character hex, or valid length with non-hex characters like "zzz". Those should return 400. If they return 500 that's the same root cause.




🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 12, 2026, 05:15:32 AM
Merited by irfan_pak10 (1)
 #85

/api/health leaks internal architecture

The health endpoint returns the full provider fallback chain, Redis status, circuit-breaker states, and the application version. Useful to plan provider-targeted DoS so the fallback chain exhausts.

Reproducer
Code:
$ curl -s https://omnisee.io/api/health | python -m json.tool
{
  "status": "healthy",
  "version": "1.0.0",
  "providers": {"blockstream":"up", "mempool":"up", ...},
  "redis": "up",
  "circuits": {
    "primary":   {"name": "blockstream", ...},
    "fallback":  {"name": "mempool", ...},
    "tertiary":  {"name": "blockchain_info", ...},
    "quaternary":{"name": "blockcypher", ...},
    "local":     {"name": "local_esplora", ...}
  }
}

Impact
•   Reveals the provider fallback order so an attacker can target the chain.
•   Reveals presence of Redis and a private/local Esplora node.
•   Reveals application version (1.0.0).

Fix
Public /api/health should return only {"status":"ok"}. Move the rich payload to /api/health/internal gated behind an admin token or Cloudflare Access policy.




In addition to the issues already mentioned, the screenshot also shows that the endpoint exposes operational state information for every fallback provider, including whether each circuit breaker is currently “closed” and how many failures have occurred. This gives a potential attacker real-time insight into backend resilience and failover behavior, not just the provider names themselves.

The disclosure of `
Code:
local_esplora
` is of particular concern. I think it's interesting because it suggests the presence of a self-hosted internal blockchain node or infrastructure component. i don't really see it directly reachable. Nevertheless this kind of information would help an attacker map the environment and prioritize further probe against potentially exposed endpoints.

Another point I believe is worth mentioning is that the response can also help optimize resource-exhaustion attacks. You can see that the exact provider sequence is visible in the screenshot (blockstream to mempool to blockchain_info to blockcypher to local_esplora). This makes it even easier for an attacker to practically degrade upstream providers one by one until traffic is forced onto fallback systems. the consequence would be increasing operational costs and reducing reliability for everyday users of the platform.

The version number (1.0.0) should never be exposed since it helps potential attackers. They can use it to search for known bugs in this version of FastAPI or any other library Omnisee app depends on, and try to exploit them.

It's obvious that this endpoint reduces attacker reconnaissance effort since internal architecture, dependency relationships, failover logic, and operational telemetry are clearly exposed through this unauthenticated public route.

🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 12, 2026, 05:31:09 AM
 #86

Backend nginx 1.29.8 version disclosed via /metrics 404

Cloudflare normally rewrites the Server header, but the upstream nginx default 404 body still renders the version in the page footer.

Reproducer
Code:
$ curl -s https://omnisee.io/metrics
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx/1.29.8</center>
</body>
</html>

Impact
•   Identifies the exact upstream nginx version for CVE targeting.

Fix
On the upstream nginx set server_tokens off; and provide a custom 404 page that does not leak the version.




I'll add that this issue appears to bypass part of Cloudflare’s normal origin masking behavior. Even though Cloudflare hides the upstream Server header, the default nginx error page in plain sight still exposes the exact backend version (
Code:
nginx/1.29.8
) clearly in the HTML response body, as shown in the screenshot. If I'm an attacker, I already have a more accurate fingerprint of the infrastructure and so targeting CVE research is a bit easier.

This could also be a pointer to a wider hardening issue rather than being limited only to
Code:
/metrics
. For me, an  attack strategy would be chaining together small disclosures like this to build a complete map of the backend stack before attempting deeper attacks.

Another important point is that the issue leaks information about the origin server even behind Cloudflare protection. While researching an infrastructure, we mostly use these seemingly minor inconsistencies to differentiate CDN behavior from origin behavior and identify misconfigurations between reverse proxy and upstream systems.

A stronger fix in my opinion would be to disable nginx version tokens entirely using
Code:
server_tokens off
, replace the default nginx error templates with generic custom pages before verifying that all error responses returned by the origin server are sanitized consistently across the application. It may also be necessary to test additional endpoints and status codes to ensure no other backend details are unintentionally exposed.

🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 12, 2026, 06:20:53 PM
Merited by irfan_pak10 (4)
 #87

HEAD / returns 405 Method Not Allowed (RFC 7231 violation)

RFC 7231 §4.3.2 requires that any resource that supports GET MUST also support HEAD. Currently CDN edge probes, link checkers, monitoring tools, and HEAD-based prefetch fail.

Reproducer
Code:
$ curl -I https://omnisee.io/
HTTP/1.1 405 Method Not Allowed
allow: GET
 
$ curl -s -o /dev/null -w "%{http_code}\n" https://omnisee.io/
200

Impact
•   Breaks monitoring tools, link checkers and CDN edge probes that issue HEAD.

Fix
On Starlette/FastAPI, add a HEAD route that mirrors GET (use methods=["GET","HEAD"]), or have nginx synthesise HEAD by stripping the body from GET.





I actually think this issue is bigger than just an RFC compliance problem. A lot of components rely on
Code:
HEAD
requests before making full
Code:
GET
requests. We are talking about uptime monitors, SEO crawlers, CDN validation systems, performance scanners, SM bots and even some wallet security integrations. Reachability tests for these start at
Code:
HEAD
. It's obvious that some automated systems may assume although incorrectly that the site is down, misconfigured, or blocking traffic. This is simply Because the server currently returns
Code:
405
Method Not Allowed while GET works normally.

The report screenshot also shows the server explicitly advertising allow
Code:
GET
, which confirms the framework routing layer is rejecting
Code:
HEAD
before application logic is reached. You may be aware that some frameworks like FastAPI,
Code:
HEAD
is not always automatically enabled. So what we have here is more of a routing configuration issue than a simple missing method.

Another detail the report doesn't have is caching/CDN behavior. Some CDN edge nodes and cache validators use
Code:
HEAD
requests to validate freshness or content existence without downloading the full response body. Returning
Code:
405
can interfere with cache warmups, health checks and optimization workflows. This can indirectly affect reliability even if users do not immediately notice the problem.

We can also view the report from the information disclosure angle. Returning a framework-generated
Code:
405
instead of handling
Code:
HEAD
may expose differences in backend behavior compared to proxied or CDN-cached responses. If I'm attacking this infrastructure, I'd be testing unsupported HTTP methods and comparing headers, allowed verbs, and response structures.

A practical improvement for this section would be testing all major routes, not just /. Patching only the homepage while API endpoints still reject HEAD wouldn't fully cut it. A good approach would be:

Enable automatic
Code:
HEAD
handling globally for all
Code:
GET
routes.
Verify responses preserve the same headers as
Code:
GET
while stripping only the body.
Ensure CDN/proxy layers do not override or block
Code:
HEAD
.
Regression tests should be added so that future routes inherit proper behavior automatically.

🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
HasanHandy
Newbie
*
Offline

Activity: 22
Merit: 0


View Profile
May 13, 2026, 02:44:33 PM
 #88

Bech32 address (if it is your first report for round#2): bc1qxl52cgc446jthut9g0fwvx5u3g0k4n29w448ag
Report Title: URLs where HSTS is not enabled: https://omnisee.io/cdn-cgi/images/
Report Details:
Vulnerability Description:
HTTP Strict Transport Security (HSTS) tells a browser that a web site is only accessable using HTTPS. It was detected that your web application doesn't implement HTTP Strict Transport Security (HSTS) as the Strict Transport Security header is missing from the response.

Discovered by HSTS not implemented

The impact of this vulnerability:
HSTS can be used to prevent and/or mitigate some types of man-in-the-middle (MitM) attacks

How to fix this vulnerability:
It's recommended to implement HTTP Strict Transport Security (HSTS) into your web application. Consult web references for more information
ContentWriter
Member
**
Offline

Activity: 445
Merit: 23

Earn from your cryptocurrencies


View Profile
May 13, 2026, 03:43:38 PM
Merited by irfan_pak10 (1)
 #89

/docs/ 307 redirect downgrades scheme to http://

HTTPS request gets a 307 with Location header pointing to plain http://. HSTS protects browsers, but the server emits an unsafe scheme — root cause: upstream FastAPI does not trust X-Forwarded-Proto.

Reproducer
Code:
$ curl -sI https://omnisee.io/docs/
HTTP/1.1 307 Temporary Redirect
location: http://omnisee.io/docs

Impact
•   Non-HSTS clients (older bots) can be downgraded to plaintext.

Fix
•   Configure Starlette with ProxyHeadersMiddleware so X-Forwarded-Proto is honoured:
Code:
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")

•   Or run uvicorn with --proxy-headers --forwarded-allow-ips '*'.






Nice find. This looks like the app is not trusting X-Forwarded-Proto, so it thinks the original request was HTTP and generates a redirect to http:/  instead of  https://.

Even though HSTS will protect most modern browsers, this can still affect non-HSTS clients  such as bots and crawlers. It can also cause unnecessary HTTP downgrade during redirects. Most likely, the backend (FastAPI/Uvicorn) isn’t correctly reading the proxy headers from the CDN or load balancer, so it thinks the request is HTTP instead of HTTPS.

The simplest fix is to enable proxy header support so the app correctly detects HTTPS behind the proxy.

🔐 No KYC Crypto Trading
💸 Earn While You Trade
👉 Join Bridgoro Now
AB de Royse777 (OP)
Copper Member
Legendary
*
Offline

Activity: 3220
Merit: 4771


Bitcointalk Campaign Manager. Telegram @Royse777


View Profile WWW
May 13, 2026, 11:51:45 PM
Merited by irfan_pak10 (4)
 #90

The campaign is CFNE now meaning no more NEW entry after this response. Those who already joined they can continue the discussion. I am yet to review the entries you made so far however this is (following) I received from the management who reviewed your entries and worked on it.

Quote
# Omnisee.io Bug Bounty — Report



## Contents

| # | Bug | Severity | Status |
|---|---|---|---|
| 1 | Bitfinex cold wallet misclassified as DANGEROUS | High | ✅ Fixed |
| 2 | AI Detective tokens terminology ambiguous | Low | ✅ Fixed |
| 3 | WP-config 403 misinterpretation | N/A | ✅ Hardened |
| 4 | /analyze rate limit missing | High | ✅ Fixed |
| 5 | /openapi.json publicly exposed | Medium | ✅ Fixed |
| 6 | /docs (Swagger UI) reachable | Medium | ✅ Fixed (= #5) |
| 7 | /api/stats/price no auth | N/A | Not-a-bug (proven) |
| 8 | /api/address/{addr} no auth | N/A | Not-a-bug (proven) |
| 9 | Block page transactions list missing | Medium | ✅ Fixed |
| 10 | /docs (duplicate of #6) | Medium | ✅ Fixed (= #5) |
| 11 | /redoc reachable (duplicate of #5) | Medium | ✅ Fixed (= #5) |
| 12 | TLS 1.0/1.1 deprecated | Medium | ✅ Cloudflare set to 1.3 |
| 13 | /block/{garbage} → 503 | Medium | ✅ Fixed |
| 14 | /api/transaction/{nonexistent} → 500 | Medium | ✅ Fixed |
| 15 | /api/block/{invalid} → 500 | Medium | ✅ Fixed |
| 16 | /api/health architecture leak | Medium | ✅ Already fixed (proven) |
| 17 | nginx 1.29.8 version disclosed | Low | ✅ Fixed |
| 18 | HEAD method 405 | Low | ✅ Fixed |
| 19 | Duplicate Strict-Transport-Security | Low | ✅ Fixed |
| 20 | 307 redirect downgrade http:// | Medium | ✅ Fixed |
| 21 | /analyze leaks LLM tokens metrics | Medium | ✅ Fixed |
| 22 | Homepage Python repr leak | Low | ✅ Fixed |
| 23 | /timeline ignores hops param | Low | ✅ Fixed |
| 24 | /analyze accepts 100KB body | Medium | ✅ Fixed |
| 25 | Genesis tx confirmations=0 | Medium | ✅ Fixed |
| 26 | /expand ignores depth/direction params | Low | ✅ Fixed |
| 27 | Cytoscape wheelSensitivity warning ×11 | Low | ✅ Fixed |
| 28 | Missing /.well-known/security.txt | Low | ✅ Fixed |
| 29 | Google Fonts CSS no SRI | Medium | ✅ Self-hosted |
| 30 | Missing COOP/COEP/CORP headers | Medium | ✅ Fixed |
| 31 | /api /static /ws → 301→404 chain | Low | ✅ Fixed |
| 32 | /scam-check timing side-channel | Low | ✅ Fixed |
| 33 | /sitemap.xml only 11 entries | Low | ✅ Improved |

---

## 1. Bitfinex cold wallet misclassified as DANGEROUS

**Request:** Address `3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r` is the Bitfinex cold storage wallet (lifetime received ~3.3M BTC). The platform was flagging it as 82 DANGEROUS due to false-positive reports in ScamBreaker DB combined with the drain-floor heuristic added in the previous fix.

**Response:** Two-layer fix:
1. `notable_addresses.py` — added the address as `kind="misattribution"` → forces score=15 TRUSTED
2. `scoring.py` — the drain-floor heuristic no longer triggers for exchange-class addresses (`tx_count > 1000` or `total_received > 100,000 BTC`)

**Verified:** `score: 15 trusted` (was 82 dangerous). Control case — a real drain-scam address `1DVhaBdb...` — still scores 75 SUSPICIOUS.

---

## 2. AI Detective tokens terminology ambiguous

**Request:**
> Transaction metrics of the AI Detective includes terms like "tokens" which have no bearing with the Bitcoin network. The right terms should be sats, BTC, transactions (txs), etc and not `Tokens: 1785 in / 146 out | Duration: 5.9s | 27.47 tok/s`. Incorrect use of terminology affects the credibility of the platform. Bitcoin-specific language and terms are appropriate for a platform like Omnisee. Sloppy use of terms gives the impression that platform supports altcoins such as ETH.
>
> Right terms should be total BTC received, BTC sent and transactions per second.

**Response:** Rewrote the footer in `address.html`:

```
Before: Tokens: 1785 in / 146 out | Duration: 5.9s | 27.47 tok/s
After:  AI inference: 1785 → 146 text tokens · 5.9s · 27.47 text tokens/s
```

With a tooltip: *"Text tokens consumed by the language model when generating this analysis. Unrelated to BTC, sats, or any cryptocurrency token."*

The reporter's premise — that these should be sats/BTC — was incorrect (these are LLM model tokens, not crypto), but the ambiguity was real on a Bitcoin platform. Disambiguated with "AI inference" prefix and "text tokens" suffix + tooltip.

(Later the metrics field was removed from the public response entirely — see #21.)

---

## 3. WP-config files 403 misinterpretation

**Request:**
> During security testing, I saw paths associated with WP configuration files that return 403 Forbidden status codes. This confirms the existence of these files which didn't return 404. Some of these include `/.wp-config.php.swp`, `/wp-config.php`, `/.wp-config.php`, and `/wp-config.php~`. These shouldn't exist on Omnisee, a platform that runs custom Bitcoin analytics and so should be cleaned out. This is evidenced by the presence of a Vim swap file (.swp) indicating incomplete cleanup.
>
> This points to the fact that server contains obsolete or forgotten but useless files or even WP installation not in use. This bug demands an audit of the server and the deletion of forgotten files.

**Response:** Not a vulnerability — the 403 came from a server-level nginx `if ($is_bot) { return 403; }` rule. Any URL hit with a bot-class User-Agent returned 403, including `/no-such-page-9999`. Filesystem audit: `find / -name "wp-config*"` → 0 results in containers and on host.

**Hardening:** Changed `return 403` → `return 444` (close connection silently). Scanners now receive no body, no headers, no differential — eliminates the false-positive vector entirely.

---

## 4. /analyze rate limit missing

**Request:**
> AI Detective endpoint is unauthenticated and unthrottled. Each call burns ~2,000 LLM tokens (7-18 s wall time). 10 sequential POSTs all returned 200. Sustained spam drains the LLM budget and slows service for real users.
>
> Impact:
> - Attacker sustains ~5-10 RPS to drain the LLM budget (potentially thousands of dollars per day in tokens).
> - Analysis queue backs up; legitimate users see 7-18 s+ latency.
> - Combines with next bug (no body schema) to amplify the attack with arbitrary payload size.

**Response:** Added per-IP rate-limit in `rate_limit.py` + middleware in `main.py`:
- `analyze_rate_per_min: 5`
- `analyze_rate_per_hour: 50`
- `_client_ip()` extracts the real client IP via `CF-Connecting-IP` (we're behind Cloudflare; `X-Real-IP` is the CF edge)
- 429 + `Retry-After` + `X-RateLimit-*` headers

**Verified:** 10 POSTs from one IP → `200 200 200 200 200 429 429 429 429 429`. A different IP gets its own independent quota.

---

## 5. /openapi.json publicly exposes 38 internal API endpoints

**Request:**
> FastAPI auto-docs were not disabled in production. The full OpenAPI 3.1 schema (21 KB) is reachable, enumerating every internal route, parameter name, and operationId.
>
> Impact:
> - Free roadmap of every endpoint without path-fuzzing.
> - Internal route names like `get_address_scam_check_api_address__address__scam_check_get` leak Python function names.
> - New endpoints are advertised the moment they are added to the codebase.

**Response:** Set `openapi_url=None, docs_url=None, redoc_url=None` on the FastAPI() init. Added an admin-gated copy at `/4b09f6d9d5355bb1/openapi.json` (404 without auth, 200 + full schema with admin cookie).

**Verified:** `/openapi.json`, `/docs`, `/redoc` all return 404. Admin path returns 200 + 38 paths when authenticated.

---

## 6, 10, 11. /docs and /redoc duplicates

**Request:** (same as #5, separately reported for `/docs` Swagger UI and `/redoc` ReDoc HTML)

**Status:** All three bugs closed by the same patch as #5. `docs_url=None` and `redoc_url=None` disable both Swagger UI and ReDoc. Verified: 404 on all three URLs through Cloudflare and origin.

---

## 7. /api/stats/price returns price without auth

**Request:**
> The public API endpoint `/api/stats/price` can return data without authentication thereby allowing unrestricted scraping and abuse. The endpoint returns current Bitcoin price data in several fiat currencies (USD, EUR, GBP, CAD, CHF, AUD, JPY), and this is done without the requisite API key, token or authentication. The consequence of this bug is automatic scraping, data abuse and potential denial of service attacks.

**Response:** Not a vulnerability — Bitcoin price is public information. The exact same numbers are available from mempool.space, CoinGecko, blockchain.com, and every crypto exchange's public ticker — without authentication. Auth is the wrong control here; there's no secret to protect.

The valid sub-concern (DoS via abuse) is already mitigated:
- Redis cache TTL 30s — verified: 50 parallel requests resulted in 0 upstream API calls
- nginx `limit_req zone=api burst=20 rate=10r/s` per IP — verified: 50 burst from one IP got 13×503
- Bot UA block — scanner-class UAs receive 444 (connection dropped)
- Cloudflare edge-level DDoS protection on top

---

## 8. /api/address/{addr} returns blockchain data without auth

**Request:**
> The endpoint `https://omnisee.io/api/address/{address}` returns Bitcoin data such as balance, transaction count, volume received and sent without due authentication, token or API key. This enables unrestricted and automated scraping and surveillance. Unlimited requests could overwhelm backend providers and should be discouraged.

**Response:** Not a vulnerability — Bitcoin blockchain data is inherently public. The same balance/tx-count for any address is freely available from mempool.space, blockstream.info, blockchain.com, every Bitcoin node's RPC. Authentication doesn't make the blockchain non-public.

The valid abuse vector (overwhelming backend providers) is already mitigated:
- Redis cache TTL 60s on `btc:addr:{address}` — verified: cache hit serves repeat requests without upstream calls
- nginx per-IP rate limit — verified: 100 hammering requests → 19×503 throttled
- Bot block, address validation, multi-provider fallback with circuit breakers
- Cloudflare edge protection

---

## 9. Block page transactions list missing

**Request:**
> Clicking the block pages on the Omnisee homepage does not reveal transaction lists. The only data available are height, hash, timestamp, size, weight, version, nonce, difficulty, merkle root, etc. The transactions in the block are not listed. Transaction table is absent. There are no pagination, and no "load more" button. So there is no way for users to view or access individual transactions within the block. This is supposed to be a core feature of any blockchain explorer.

**Response:** The transaction list and pagination DO exist in the template (verified: page 1 of `/block/948438` returns 25 tx links + paginator "Page X of 172"). Two edge cases hid it from the reporter:

1. URL with a thousand-separator comma (`/block/948,438` as displayed on the homepage) → 503 (Esplora doesn't accept commas).
2. When all upstream providers were simultaneously rate-limited, `txs_error` showed a small red badge that was easy to miss.

**Fix:**
- Strip comma + whitespace from the path param: `hash_or_height = hash_or_height.replace(",", "").replace(" ", "").strip()`
- Prominent error UX: large card with "⚠ Transactions temporarily unavailable" + Retry button (instead of a small red badge)

**Verified:** Both `/block/948438` and `/block/948,438` return 200 + 25 tx links + paginator.

---

## 12. TLS 1.0/1.1 deprecated

**Request:**
> RFC 8996 (March 2021) deprecates TLSv1.0/1.1. Modern browsers refuse them, but legacy bots and downgrade-attack tooling still negotiate them. PCI-DSS, NIST SP 800-52r2 disallow them.
>
> Fix: Cloudflare → SSL/TLS → Edge Certificates → Minimum TLS Version → set to TLS 1.2 (or 1.3).

**Response:** This is a Cloudflare edge setting — origin nginx was already on `TLSv1.2 TLSv1.3` only (`nginx.conf:196, :288`). The fix lives in the Cloudflare dashboard, not in the code.

**Action taken:** User set Minimum TLS Version to 1.3.

---

## 13. /block/{invalid_format} returns 503 'Service Unavailable'

**Request:**
> Garbage block-id input crashes the backend, surfaced as an alarming 503 'Service Unavailable' page instead of a graceful 400 / 404. Users hitting a typo see a scary error suggesting site outage.
>
> Reproducer: `/block/abc`, `/block/notablock`, `/block/-1`, `/block/0x123` → all 503. Control: `/block/999999999` → 404 (correct).
>
> Impact: pollutes monitoring; clients can't distinguish bad input from real outage.

**Response:** Added pre-validation in `pages.py` block route:
```python
_HEX64_RE = re.compile(r"^[0-9a-fA-F]{64}$")
def _validate_block_id(s: str) -> bool:
    if _HEX64_RE.match(s): return True
    if s.isdigit() and 0 <= int(s) < 10**9: return True
    return False
```

Garbage → 400 "Invalid Block Identifier" with a friendly message. Bonus fix: upstream HTTP 4xx (Esplora rejecting valid-format-but-nonexistent block) is now mapped to 404, not 503.

**Verified:** All reporter cases (`abc`, `notablock`, `-1`, `0x123`, `deadbeef`, `AAAA`) → 400. Valid `/block/948438` and `/block/{64-hex hash}` → 200. Valid format but no such block (`/block/999999999`) → 404.

---

## 14. /api/transaction/{nonexistent} returns 500 instead of 404

**Request:**
> Valid 64-hex-format but nonexistent txids trigger an unhandled backend exception. API returns bare 'Internal Server Error' (text/plain, 21 bytes). The HTML route `/tx/{nonexistent}` correctly returns 200 with a 'Transaction Not Found' page — handlers diverge.

**Response:** Root cause: `_call_with_fallback` re-raises `httpx.HTTPStatusError(404)` when all upstream providers return 404, but the route only caught `AddressNotFoundError`. Added explicit handlers:
```python
except httpx.HTTPStatusError as e:
    if e.response.status_code in (400, 404):
        return JSONResponse(404, {"error":"Transaction not found", "code":"TX_NOT_FOUND"})
    return JSONResponse(503, {...})
except Exception:
    return JSONResponse(503, {"code":"TX_LOOKUP_FAILED"})
```

Also fixed a pre-existing bug noticed in passing: the `AddressNotFoundError` branch was returning 400 with `code: TX_NOT_FOUND` — status code didn't match the semantic. Now returns 404.

**Verified:** All-1s, all-fs, random nonexistent → 404 `TX_NOT_FOUND` JSON. Real txid → 200 with full body.

---

## 15. /api/block/{invalid} returns 500

**Request:**
> `/api/block` crashes the handler for negative integers, alphabetic, hex-prefixed, and out-of-range integers. `/api/block/0` (genesis) and valid heights work fine. Same root cause class as the previous 2 bugs.

**Response:** Same fix pattern as #13 + #14: pre-validation regex + httpx.HTTPStatusError mapping + generic Exception catch-all + thousand-separator comma stripping.

```python
_HEX64_RE = re.compile(r"^[0-9a-fA-F]{64}$")
def _valid_block_id(s: str) -> bool: ...
# 400 INVALID_BLOCK_ID for garbage; 404 BLOCK_NOT_FOUND for upstream 4xx;
# 503 PROVIDERS_UNAVAILABLE for upstream 5xx; 503 BLOCK_LOOKUP_FAILED for unknown errors.
```

**Verified:** All garbage cases → 400, valid heights/hashes → 200, nonexistent valid format → 404. `/api/block/0` (genesis) and `/api/block/948,438` (with comma) → 200.

---

## 16. /api/health architecture leak

**Request:**
> The health endpoint returns the full provider fallback chain, Redis status, circuit-breaker states, and the application version. Useful to plan provider-targeted DoS so the fallback chain exhausts.

**Response:** ✅ Already fixed in a previous round. Public `/api/health` returns only `{"status":"ok"}` (15 bytes) or `{"status":"degraded"}` (503). Detailed diagnostics — provider names, fallback order, circuit-breaker states, Redis status, application version — moved to `/api/admin/health`, gated behind admin auth.

**Verified:** Public path body is 15 bytes; admin path returns 401 without auth.

---

## 17. nginx 1.29.8 version disclosed via /metrics 404

**Request:**
> Cloudflare normally rewrites the Server header, but the upstream nginx default 404 body still renders the version in the page footer.
>
> Reproducer: `curl https://omnisee.io/metrics` → `<center>nginx/1.29.8</center>`
>
> Impact: identifies the exact upstream nginx version for CVE targeting.

**Response:** Added `server_tokens off;` to the `http {}` block of nginx.conf:

```
Before: <center>nginx/1.29.8</center> + Server: nginx/1.29.8
After:  <center>nginx</center>          + Server: nginx
```

Deployed by `docker restart omnisee-nginx-1` (not reload — single-file bind mount + atomic-write reissues inode; restart needed to repick).

---

## 18. HEAD / returns 405 Method Not Allowed (RFC 7231 violation)

**Request:**
> RFC 7231 §4.3.2 requires that any resource that supports GET MUST also support HEAD. Currently CDN edge probes, link checkers, monitoring tools, and HEAD-based prefetch fail.

**Response:** Added one middleware in `main.py` that rewrites HEAD → GET in the scope, runs the handler, drains the streaming body to compute its length, and returns an empty body with the same status + headers and a correct Content-Length:

```python
@app.middleware("http")
async def _head_method_mw(request, call_next):
    if request.method != "HEAD":
        return await call_next(request)
    request.scope["method"] = "GET"
    response = await call_next(request)
    body_len = sum(len(c) async for c in response.body_iterator)
    return Response(content=b"", status_code=response.status_code,
                    headers={**response.headers, "content-length": str(body_len)})
```

**Verified:** HEAD on all routes returns the same status as GET (200/404/400). `Content-Length` matches GET body size — RFC 7231 §4.3.2 compliant.

---

## 19. Duplicate Strict-Transport-Security headers

**Request:**
> Two HSTS headers are emitted on every response, identical value but different case. Indicates two layers of middleware/proxy both injecting the header. Pick one place to emit HSTS.

**Response:** Made the FastAPI middleware the single canonical source; removed `add_header Strict-Transport-Security` from nginx.conf.

**Caveat:** First attempt deleted HSTS from FastAPI and kept it in nginx — broke HSTS on all `/api/*` paths because nginx's `location /api/` block has its own `add_header X-Cache-Status`, which (per nginx's shadowing rule) disables inheritance of any `add_header` from the parent server block. App-level middleware applies uniformly regardless of location.

**Verified:** Exactly 1 HSTS header on every endpoint (homepage, `/api/*`, `/block/*`, 404s, through Cloudflare).

---

## 20. /docs/ 307 redirect downgrades scheme to http://

**Request:**
> HTTPS request gets a 307 with Location header pointing to plain `http://`. HSTS protects browsers, but the server emits an unsafe scheme — root cause: upstream FastAPI does not trust X-Forwarded-Proto.

**Response:** Root cause confirmed: uvicorn was launched without `--proxy-headers`, so `request.url.scheme` reflected the socket scheme (`http`, since nginx terminates TLS) instead of the X-Forwarded-Proto from nginx. Any absolute redirect (Starlette's `redirect_slashes`, mount redirects) used the wrong scheme.

```python
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
app.add_middleware(ProxyHeadersMiddleware, trusted_hosts="*")
```

Mounted as outer-most so the corrected scheme is visible to every downstream handler.

**Verified:** `/4b09f6d9d5355bb1/login/` (trailing-slash) → 307 → `https://omnisee.io/...` (was `http://`). Relative redirects (most of our handlers) were already safe (no scheme).

---

## 21. /analyze response leaks LLM token counts and eval rate

**Request:**
> The JSON returned by `/api/address/{addr}/analyze` contains a 'metrics' field with `tokens_in`, `tokens_out`, `duration_ms`, `eval_rate`. Internal observability data should not be sent to clients.
>
> Impact:
> - Lets attackers measure cost-per-call and tune token-drain attacks (Bug #1).
> - `eval_rate ≈ 27 tok/s` fingerprints a small local LLM (e.g., Llama-3 8B on a single GPU).

**Response:** Added a helper in `llm_detective.py`:
```python
def _strip_metrics_for_public(response, address):
    m = response.get("metrics")
    if m:
        logger.info("ai_detective metrics addr=%s tokens_in=%s tokens_out=%s "
                    "duration_ms=%s eval_rate=%s", address[:12], ...)
    return {k: v for k, v in response.items() if k != "metrics"}
```

Applied at both return paths (cache-hit and final). Flushed Redis cache so legacy entries (which still contained metrics) don't serve them.

**Verified:** Response keys `['address', 'analysis', 'score']` (no `metrics`). Server logs preserve the data as a structured `INFO` line for the admin LLM dashboard.

---

## 22. Homepage HTML comment leaks raw Python data structure

**Request:**
> An f-string interpolation in a header HTML comment rendered a Python list-of-dicts (single quotes, not JSON). Confirms backend is Python and exposes the internal price-badge data shape.
>
> Reproducer: `curl https://omnisee.io/ | grep "server-rendered from"` → `[{'cur': 'USD', 'sym': '$', 'formatted': '$80,856'}, ...]`

**Response:** In `base.html` two leak points (HTML comment + JS comment) had `{{ btc_prices() }}` rendering Python's `repr()` of a list of dicts. Replaced with static descriptive text — the actual server-rendered loop over `<ul class="price-menu">` below still uses the same fields normally (no repr leak).

**Verified:** 0 matches for `[{&#39;cur` / `{'cur'` on any page (homepage, /faq, /block/, /address/).

---

## 23. /api/graph/{addr}/timeline ignores hops parameter

**Request:**
> Negative, zero, very large, alphabetic, null, and even `<script>` values for `?hops=` all return 200 with the same body. The parameter is unused or schema validation is missing.

**Response:** `/timeline` is a flat tx history (not graph traversal) — it never accepted `hops` at all. FastAPI silently ignores unknown query params, which masks the misconfiguration since sibling routes (`/graph/{addr}`, `/export/json`) do accept `hops`. Added explicit allowlist check:
```python
_allowed = {"max_txs"}
_extra = set(request.query_params.keys()) - _allowed
if _extra:
    return JSONResponse(400, {"error": ..., "code": "UNKNOWN_PARAM", "allowed": ...})
```

**Verified:** All reporter cases → 400 with a clear message pointing callers to `/api/graph/{address}` for hops/depth control.

---

## 24. /analyze accepts arbitrary 100 KB body silently

**Request:**
> POST body is parsed but ignored at the application layer. Combined with Bug #1, an attacker can stuff huge bodies to amplify resource use.

**Response:** In the analyze middleware (before rate-limit check, so a body-DoS doesn't even consume a quota slot):
```python
cl = request.headers.get("content-length")
if cl and cl.isdigit() and int(cl) > 32:
    return Response(b'{"error":"...","code":"BODY_NOT_ALLOWED","max_bytes":32}', 400)
```

32-byte allowance covers `{}`, `null`, `""` — common idle markers clients send reflexively. Anything bigger is unintended. Chunked transfer-encoding also rejected defensively.

**Verified:** 100KB → 400, 33-byte → 400, `{}` → 200, no body → 200.

---

## 25. Genesis transaction confirmations = 0 (off-by-falsy)

**Request:**
> The famous Bitcoin genesis transaction returns `confirmations=0` even though it has hundreds of thousands of confirmations. Block-100000 control case computes correctly. Likely an explicit special-case for `block_height==0`.

**Response:** Classic Python falsy-trap. In `blockchain.py`:
```python
# Before:
if block_height and tip_height:  # block_height == 0 (genesis) → falsy → skip!
    confirmations = tip_height - block_height + 1

# After:
if block_height is not None and tip_height:
    confirmations = tip_height - block_height + 1
```

Applied at both sites (`get_transaction` + `get_address_transactions`).

**Verified:** Genesis (4a5e1e4b...) → `confirmations=948521` (= tip 948520 + 1). Control case block#100000 (8c14f0db...) → 848521. Flushed Redis cache on the genesis tx so stale `0` doesn't get served.

---

## 26. /api/graph/{addr}/expand ignores depth and direction parameters

**Request:**
> Identical responses for `depth=-1`, `depth=99999`, `depth=abc`, `direction=up`, `direction=evil`. Parameters not validated by Pydantic.

**Response:** Same pattern as #23. `/expand` is a one-hop endpoint (no recursive BFS), so depth/direction don't apply — but the params were silently ignored. Explicit allowlist:
```python
_allowed = {"max_txs", "exclude_txids"}
```

Error message points callers to `/api/graph/{address}` for multi-hop traversal.

**Verified:** All reporter cases → 400. Valid `?max_txs=5` → 200.

---

## 27. Cytoscape custom-wheel-sensitivity warning spam ×11

**Request:**
> Custom wheelSensitivity makes mouse-wheel zoom feel unnatural for most users (Cytoscape author warns against it). Warning fires 11 times per single page load.

**Response:** Removed `wheelSensitivity: 0.3` from the `cytoscape()` init in `graph_interactive.html:682`. Cytoscape now uses the default sensitivity, computed for mainstream hardware.

**Verified:** 0 `wheelSensitivity` references in served HTML. Console warnings on load: 11 → 0.

---

## 28. No /.well-known/security.txt (RFC 9116)

**Request:**
> RFC 9116 standard for vulnerability disclosure missing. Both `/.well-known/security.txt` and `/security.txt` return 404. Researchers cannot find a way to report responsibly.

**Response:** Added a FastAPI handler in `pages.py`:
```python
_SECURITY_TXT = """Contact: mailto:startercasino@gmail.com
Expires: 2027-05-08T00:00:00Z
Preferred-Languages: en, ru
Canonical: https://omnisee.io/.well-known/security.txt
"""

@router.get("/.well-known/security.txt", include_in_schema=False)
@router.get("/security.txt", include_in_schema=False)
async def security_txt():
    return PlainTextResponse(_SECURITY_TXT, media_type="text/plain; charset=utf-8")
```

**Verified:** Both URLs return 200 + `text/plain; charset=utf-8`. RFC 9116-valid minimal contract; optional PGP key / policy URL / dedicated mailbox can be added later.

---

## 29. Google Fonts CSS link lacks Subresource Integrity (SRI)

**Request:**
> External stylesheet from `fonts.googleapis.com` is loaded without `integrity=` or `crossorigin=` attribute. If `fonts.googleapis.com` is compromised, attackers can serve malicious CSS that exfiltrates data via background-image URLs or `input[type=password]` selectors.

**Response:** Self-hosted the fonts. Build script `selfhost_fonts.py`:
1. Download upstream CSS with a Mozilla UA (so Google serves WOFF2)
2. Parse `@font-face` blocks; keep only `latin` + `latin-ext` subsets (UI is English-only — vietnamese/cyrillic/greek are dead weight)
3. Download 6 unique WOFF2 files into `/static/fonts/` with URL-hash filenames (variable fonts share one file across weights — key by URL only)
4. Rewrite CSS so `src: url(...)` points to `/static/fonts/...`
5. Replace three external `<link>` tags in `base.html` (preconnect + preconnect + stylesheet) with one self-hosted link
6. Trim CSP — `style-src` and `font-src` no longer whitelist `fonts.googleapis.com` / `fonts.gstatic.com`

**Verified:** 0 Google references in HTML. Page works through Cloudflare. Bonus: ~50KB saved on dropped subsets; 1 TLS round-trip less on first paint; offline-friendly; no Google tracking of page visits.

---

## 30. Missing Cross-Origin-Opener-Policy / Embedder-Policy / Resource-Policy

**Request:**
> Modern cross-origin isolation headers (COOP / COEP / CORP) are absent. Page is exposed to Spectre-class side-channel attacks and cannot use SharedArrayBuffer or high-resolution timers safely.

**Response:** Pre-deploy verified that cdnjs (the Cytoscape host on `/graph/.../interactive`) returns `Cross-Origin-Resource-Policy: cross-origin` — compatible with COEP `require-corp`. Added three headers in the security middleware:
```python
response.headers.setdefault("Cross-Origin-Opener-Policy", "same-origin")
response.headers.setdefault("Cross-Origin-Embedder-Policy", "require-corp")
response.headers.setdefault("Cross-Origin-Resource-Policy", "same-site")
```

Self-hosted fonts (post-#29) are same-origin, so no COEP issues there.

**Verified:** All three headers present on every response. `/graph/.../interactive` still loads cdnjs Cytoscape. Site is now `crossOriginIsolated`-eligible, unlocking `SharedArrayBuffer` and high-resolution timers for future work, plus Spectre-class side-channel protection.

---

## 31. /api /static /ws emit 301 → trailing-slash → 404 (useless chain)

**Request:**
> Three URL paths emit '301 Moved Permanently' redirecting to a trailing-slash variant that immediately 404s. The 301 chain wastes a round-trip and confuses crawlers.

**Response:** Default nginx behavior for prefix-locations with a trailing slash. Added exact-match returns before the proxy locations:
```nginx
location = /api    { return 404; }
location = /static { return 404; }
location = /ws     { return 404; }
```

**Verified:** Both bare and trailing-slash forms return direct 404 (no redirect chain). Real assets (`/static/css/fonts.css`, `/api/health`) remain 200 — the exact-match `=` modifier wins only on the literal `/api`, `/static`, `/ws` paths.

---

## 32. /api/address/{addr}/scam-check leaks cache state via response timing

**Request:**
> Timing differential between cached/uncached scam-db lookups (0.5 s for cache hit vs 2-3 s for first-time uncached). Side-channel oracle on whether an address has previously been queried.

**Response:** Two-layer fix:
1. Cache-Control: `private, no-store, no-cache, must-revalidate` for `/scam-check` → kills nginx proxy_cache + Cloudflare caching. Every request reaches the backend.
2. Constant-time middleware with a 200ms floor → masks the backend Redis-vs-SQLite micro-differential (~50ms).

**Verified:**
- in-DB: `[317, 204, 203, 203, 211] ms`
- not-in-DB: `[203, 203, 204, 206, 202] ms`

Differential ~7ms (network jitter level) — no longer a side-channel.

---

## 33. /sitemap.xml lists only 10 latest blocks — site is invisible to search

**Request:**
> Sitemap contains 11 `<loc>` entries: the homepage and the latest 10 blocks. No `/address/{x}`, no `/tx/{x}`, no `/faq`, no older blocks. For a Bitcoin explorer with hundreds of thousands of indexable pages, this means search engines see almost nothing.

**Response:** Replaced single-file sitemap with the canonical sitemap-index pattern:
- `/sitemap.xml` — `<sitemapindex>` referencing 2 sub-sitemaps
- `/sitemaps/static.xml` — `/`, `/faq`, `/.well-known/security.txt`, Tor onion (4 URLs)
- `/sitemaps/blocks.xml` — last 1000 blocks (tip-enumeration instead of paginated API: heights are sequential integers, no need for 100 round-trips to fetch them)

**Caveat:** The current `robots.txt` blocks all major search engines (Google, Bing, Yandex, Baidu, DDG, Sogou + AI crawlers). SEO benefit of the improved sitemap is 0 until `robots.txt` is flipped. The 1.8M scam-DB addresses are deliberately NOT in the sitemap — publishing them would leak the whole DB (the core product asset); if the operator decides to make the scam list publicly indexable, `/sitemaps/addresses-{i}.xml` can be added in a follow-up.

**Verified:** 11 → 1004 indexable URLs.

---

## Infrastructure improvements (meta)

### SSH / fail2ban hardening (mid-session)

fail2ban kept tripping during rapid `sshpass`+password loops.

**Solution:**
1. Generated `~/.ssh/id_ed25519` keypair
2. Installed public key in `/root/.ssh/authorized_keys` on omnisee
3. `~/.ssh/config` alias `omnisee` + ControlMaster (instant reconnects)
4. Whitelisted my egress IPs `91.205.107.50` and `192.145.39.167` in `/etc/fail2ban/jail.local`

After this, all deploys ride a single ControlMaster connection — fail2ban no longer sees us.

### Nginx restart vs reload (single-file bind mount trap)

Hit several times: edit `nginx.conf` on host → `docker exec nginx -s reload` — changes do NOT apply. Cause: `/root/omnisee/nginx.conf` is bind-mounted into the container as a single file. Atomic-write via temp-file (`sed -i` / Python `open("w")`) changes the inode → the bind mount keeps pointing at the old inode.

**Workaround:** `docker restart omnisee-nginx-1` (not `nginx -s reload`) after editing nginx.conf.

### Cache flush patterns

After cache-related fixes (HSTS, /scam-check, /timeline) I flushed `/var/cache/nginx/api/*` to prevent old TTL'd responses from serving until natural expiry.

---

## Files changed (by directory)

| Path | Bugs |
|---|---|
| `app/main.py` | #4, #5, #18, #20, #21 (via llm_detective), #24, #29 (CSP), #30, #32, #33 |
| `app/routers/admin.py` | #5 |
| `app/routers/address.py` | #4 (via middleware) |
| `app/routers/block.py` | #15 |
| `app/routers/graph.py` | #23, #26 |
| `app/routers/pages.py` | #9, #13, #28 |
| `app/routers/transaction.py` | #14 |
| `app/services/blockchain.py` | #25 |
| `app/services/llm_detective.py` | #21 |
| `app/services/notable_addresses.py` | #1 |
| `app/services/scoring.py` | #1 |
| `app/rate_limit.py` | #4 |
| `app/templates/address.html` | #2 |
| `app/templates/base.html` | #22, #29 |
| `app/templates/block.html` | #9 |
| `app/templates/graph_interactive.html` | #27 |
| `app/static/css/fonts.css` | #29 (new) |
| `app/static/fonts/*.woff2` | #29 (6 new) |
| `nginx.conf` | #3, #17, #19, #31 |
| `~/.ssh/{id_ed25519,config}` | infra |
| `/etc/fail2ban/jail.local` (origin) | infra |

---

## Outstanding items for the operator

### 1. robots.txt strategy (Bug #33)
The current `robots.txt` blocks all search engines. The improved sitemap is technically correct, but SEO impact is 0 until that's flipped. Two paths:
- **Path A — keep private:** Leave robots.txt as is; sitemap is for direct submission to Search Console / archives.
- **Path B — public discoverability:** Flip robots.txt + add `/sitemaps/addresses-{i}.xml` (1.8M scam addresses paginated 50k/file → ~36 files).

### 2. Optional security.txt extensions (Bug #28)
Can add:
- PGP key (`Encryption: ...pgp.asc`) — can generate
- Dedicated `security@omnisee.io` mailbox via Cloudflare Email Routing
- `/security-policy` URL with responsible-disclosure terms
- Bitcointalk thread URL as a second `Contact:` line

---

## Summary

**33 bug reports processed** in a single session. Of those:
- **27 code fixes** deployed and verified live
- **3 already-fixed** (closed in previous rounds, re-verified)
- **2 not-a-bug** (proven via technical analysis: #7, #8)
- **1 Cloudflare-side fix** (#12 TLS — set to 1.3 by operator)

**All verifications were live**, via `https://127.0.0.1` (origin) and `https://omnisee.io` (through Cloudflare).

Those who entered are encouraged to check the update and give their opinions. I will review your opinion and the entried that made before this post and collect them on the spreadsheet after a few days.

Cheers,

On a side note: Does anyone know ZachXBT? If yes please then Omnisee management requests to send a link of their project to him. They would love to hear his thoughts. Their develop team respects him a lot.

██████▄██▄███████████▄█▄
█████▄██▒███▄████▄▄▄█
███████▒█▒▒██████████
████▐█████▒▒▒▒▒▒▒▒▒▒████
████████▒▒▒▒▒▄▄▄▄███████
██▄████▒▒▒▒▒███▀█▀▀█▄▄▄█
▀████▒▒▒███▄█████▄▄█████▀██
█████▒▒▒██▄████▀██▄▀▀▀█████▄
███▒▒▒███████▐█▄▀▄███▀██▄
███████▄▄▄███▌▌█▄▀▀███████▄
▀▀▀███████████▌██▀▀▀▀▀█▄▄▄████▀
███████▀▀██████▄▄██▄▄▄▄███▀▀
████████████▀▀▀██████████
BETFURY
▄███████████████████▄
█████████████████████
█████████████████████
█████████████████████
█████████████████████
█████████████████████
█████████████████████
█████████████████████
█████████████████████
█████████████████████
▀███████████████████▀
CASINO  
+8,000 GAMES

▄███████████████████▄
██████████░░░████████
██████████░░░░███████
███░░░░███░░░▒▒▒▒▒███
██░░░░░░█████▒▒▒▒▒▒██
██░░░░░███████▒▒▒▒▒██
████░░██████░░░▒▒████
█████████░░░░░░░████
██████████░░░░░░░████
█████████████░░██████
▀███████████████████▀
SPORTS
 BEST ODDS
 
WELCOME BONUS
UP TO 590% + 225 FS
[ Play Now ]
Italian Panic
Hero Member
*****
Offline

Activity: 1022
Merit: 571



View Profile WWW
May 14, 2026, 09:01:18 AM
 #91


...

Those who entered are encouraged to check the update and give their opinions. I will review your opinion and the entried that made before this post and collect them on the spreadsheet after a few days.

Cheers,

On a side note: Does anyone know ZachXBT? If yes please then Omnisee management requests to send a link of their project to him. They would love to hear his thoughts. Their develop team respects him a lot.

Most of the issues have been resolved

About ZachXBT: If you mean in person, that’s impossible because he values his anonymity highly. I follow him on X and study his posts in depth to improve myself. Try contacting him there, he has a Telegram address.

About the bug hunt campaign: I don't really agree with the classification provided by management and the exclusion of other entries, but I have full confidence in you as our intermediary.

As always, thank you ABR777

Pages: « 1 2 3 4 [5]  All
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!