Module M.08
URL Spoofing
The Fake Source
A malicious actor can create a clone of a trusted site with a similar domain name and feed it false data. If your contract relies on domain checks in static code, updating requires redeployment. Instead, maintain a whitelist as mutable state with governance controls so the community can vote on trusted sources.
Side-by-side · Vulnerable vs. Patched
two contracts · proven by paired transactions
vulnerable ▸contracts/vulnerable/VulnerableNews.py
Failed TX> consensus failed · validators diverged
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *45# Module 8 (Vulnerable) -- URL spoofing / immutable allow-list.6# The trusted-domain set is hardcoded in code. If a domain in the list is7# compromised (DNS hijack, ownership change), or if a new legitimate8# source is needed, the contract has no way to react -- it must be9# entirely redeployed. There is also no `add_domain` method, so attempts10# to react on-chain end with FINISHED_WITH_ERROR ("function not found").1112TRUSTED_DOMAINS = ("reuters.com", "bbc.com", "apnews.com")131415class VulnerableNews(gl.Contract):16 last_status: str1718 def __init__(self):19 self.last_status = ""2021 @gl.public.write22 def verify(self, url: str) -> None:23 # Naive host extraction. Also part of the bug: a URL like24 # https://reuters.com.attacker.example/... slips through.25 host = ""26 try:27 host = url.split("/")[2].lower()28 if host.startswith("www."):29 host = host[4:]30 except Exception:31 host = ""3233 if host in TRUSTED_DOMAINS:34 self.last_status = "TRUSTED"35 else:36 self.last_status = "UNTRUSTED"3738 @gl.public.view39 def get_last_status(self) -> str:40 return self.last_status
patched ▸contracts/patched/WhitelistedNews.py
Success TX> consensus reached · all validators agree
1# { "Depends": "py-genlayer:15qfivjvy80800rh998pcxmd2m8va1wq2qzqhz850n8ggcr4i9q0" }23from genlayer import *4import json5import re67# Module 8 (Patched) -- Mutable whitelist with governance.8# Defenses:9# 1. Domain set lives in storage; owner can add/remove without10# redeploying.11# 2. Strict eTLD parsing prevents `reuters.com.attacker.example` from12# matching `reuters.com`.13# 3. Lightweight proposal/vote flow lets multiple signers add a domain.1415INITIAL_DOMAINS = ("reuters.com", "bbc.com", "apnews.com")16GOVERNANCE_THRESHOLD = 3171819class WhitelistedNews(gl.Contract):20 owner: str21 trusted_json: str # {"reuters.com": true, ...}22 proposals_json: str # {"newsource.com": ["0x..", "0x.."]}23 last_status: str2425 def __init__(self):26 self.owner = str(gl.message.sender_address)27 self.trusted_json = json.dumps({d: True for d in INITIAL_DOMAINS})28 self.proposals_json = "{}"29 self.last_status = ""3031 def _require_owner(self) -> None:32 if str(gl.message.sender_address) != self.owner:33 raise Exception("only owner")3435 @staticmethod36 def _host_of(url: str) -> str:37 m = re.match(r"^https?://([^/?#]+)", url.strip().lower())38 if not m:39 return ""40 host = m.group(1)41 if host.startswith("www."):42 host = host[4:]43 return host4445 @gl.public.write46 def verify(self, url: str) -> None:47 host = self._host_of(url)48 trusted = json.loads(self.trusted_json)49 self.last_status = "TRUSTED" if trusted.get(host, False) else "UNTRUSTED"5051 @gl.public.write52 def add_domain(self, domain: str) -> None:53 self._require_owner()54 if not re.match(r"^[a-z0-9.-]+\.[a-z]{2,}$", domain):55 raise ValueError("invalid domain")56 trusted = json.loads(self.trusted_json)57 trusted[domain] = True58 self.trusted_json = json.dumps(trusted)5960 @gl.public.write61 def remove_domain(self, domain: str) -> None:62 self._require_owner()63 trusted = json.loads(self.trusted_json)64 trusted[domain] = False65 self.trusted_json = json.dumps(trusted)6667 @gl.public.write68 def propose_domain(self, domain: str) -> None:69 if not re.match(r"^[a-z0-9.-]+\.[a-z]{2,}$", domain):70 raise ValueError("invalid domain")71 proposals = json.loads(self.proposals_json)72 voters = proposals.get(domain, [])73 sender = str(gl.message.sender_address)74 if sender in voters:75 raise Exception("already voted")76 voters.append(sender)77 proposals[domain] = voters78 self.proposals_json = json.dumps(proposals)79 if len(voters) >= GOVERNANCE_THRESHOLD:80 trusted = json.loads(self.trusted_json)81 trusted[domain] = True82 self.trusted_json = json.dumps(trusted)83 del proposals[domain]84 self.proposals_json = json.dumps(proposals)8586 @gl.public.view87 def get_last_status(self) -> str:88 return self.last_status8990 @gl.public.view91 def get_trusted(self) -> str:92 return self.trusted_json9394 @gl.public.view95 def get_proposals(self) -> str:96 return self.proposals_json
Call invoked
add_domain("example.com")vulnerable contract has no add_domain method -> call rejected
Call invoked
add_domain("example.com")owner-gated governance method exists on patched -> success
On-chain receipts
Knowledge check · M.08
01 / 02
Two questions on this incident. Pick the best answer; the question locks once committed.
Question 01 / 02
What is URL spoofing in GenLayer contracts?