Skip to content

joelouf/doppelshield

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

112 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DoppelShield / URL Forensics & Homograph Detection

Mozilla ObservatorySSL LabsCICodeQLLicense: Apache 2.0


Try it live → doppelshield.com


DoppelShield is a URL forensics tool that detects homograph attacks, where a deceptive link uses visually similar characters from non-Latin scripts to impersonate a legitimate domain (for example a Cyrillic а standing in for a Latin a). Paste a link and it decodes the host, flags every look-alike character with its Unicode codepoint, script, and the Latin letter it imitates, and matches the destination against a large list of commonly impersonated brands. It then traces the redirect chain hop by hop and reports whether the destination responded.

The fetching is a deliberately constrained server-side request primitive: outbound requests are guarded by connect-time SSRF defense in depth, and every failure collapses to one uniform outcome, so the endpoint cannot be turned into an internal-network oracle. The full design is written up as a threat model.

DoppelShield ships as a web scanner and a JSON API, and deploys either on a serverless platform or as a self-contained Docker service, built on Next.js 16 and Node.js 24.


 Table of Contents


Screenshots

DoppelShield homepage: the URL scanner under the headline Unmask deceptive domains

Flagged look-alike: A paypal.com impostor written with Cyrillic р (U+0440) and а (U+0430). The report decodes the host, shows its xn-- A-label, and names each confusable glyph with its codepoint, script, and the Latin letter it imitates.

Scan result: a Cyrillic paypal.com look-alike is FLAGGED, with per-glyph evidence showing U+0440 and U+0430 as Cyrillic

Benign internationalized domain: A legitimate IDN, münchen.de. The decoded host is surfaced for inspection and the redirect chain is traced to its destination, but no impersonation signal is raised, so the verdict is REVIEW rather than a flag.

Scan result: the IDN muenchen.de returns a REVIEW verdict with an informational IDN signal and a traced three-hop redirect chain

Clean pass: No look-alike signals, and the redirect chain resolves in a single HTTPS hop. A clean result means no deception was detected in the name, not that the destination is safe.

Scan result: github.com returns a CLEAR verdict over a single secure HTTPS hop

Leak-free SSRF guard: The cloud metadata endpoint (169.254.169.254) returns the same uniform no-response verdict as any unreachable host, so the scanner cannot be turned into an internal-network probe.

Scan result: the cloud metadata address returns one uniform, leak-free no-response verdict

Detection & scope

  • Decodes the host from its IDNA `xn--` A-label back to Unicode before analysis.
  • Flags homoglyphs across Cyrillic, Greek, and other confusable scripts, reporting each with its exact Unicode codepoint and script.
  • Computes a Unicode UTS #39 style skeleton and matches it against a large list of commonly impersonated brands (roughly 3000 domains) to surface look-alikes.
  • Follows redirects one hop at a time, each re-validated, with loop detection and a wall-clock deadline, and reports the full chain.

Scope and non-goals

A CLEAR verdict means no known look-alike signals were detected in the name, not that the destination is safe. Detection follows a curated confusables table spanning Cyrillic, Greek, and other scripts confusable with Latin, a subset of the full Unicode confusables set; pure-ASCII typosquats (for example paypa1.com) are out of scope, as are malware, phishing-content, and reputation verdicts. DoppelShield analyzes names and transport, and says so.

Security

DoppelShield is, by design, a server-side request primitive exposed to anonymous callers, so the engineering goal is to make that primitive useless for anything but its stated purpose. A CLEAR verdict means no look-alike signals were detected in the name, never that the destination is safe.

  • SSRF defense in depth: a scheme and port allowlist re-asserted at every redirect hop, resolve-all DNS checked against a denylist covering loopback, private, link-local, CGNAT, and cloud-metadata ranges, connect-time IP pinning that defeats DNS rebinding, a bounded walk, and a response body destroyed unread.
  • Oracle resistance and timing floor: every failure, whether a denylist hit, NXDOMAIN, refused connection, or timeout, collapses to one uniform unreachable outcome with finalUrl and status withheld, padded to a production minimum response time (ADR-0001).
  • Abuse and availability: an edge WAF rate rule, an in-process per-client limiter, a concurrency cap, and a capped streaming body read, cheapest rejection first.
  • Browser surface: a strict per-request CSP nonce with strict-dynamic and no unsafe-inline for scripts, plus HSTS, COOP, CORP, and a restrictive Permissions-Policy (ADR-0003).
  • Supply chain and runtime integrity: SHA-pinned base images and Actions, a blocking Trivy release gate on fixable HIGH or CRITICAL findings, an SBOM with mode=max provenance and a Sigstore attestation, a digest-verified GHCR to ECR mirror, and a distroless non-root runtime with no shell or package manager.
  • Cloud and IAM posture: no IAM task role to steal, GitHub OIDC scoped to v* release tags with no long-lived keys, and service control policies enforced from a separate management account.

Each control cites its implementing file in the threat model. Vulnerability reporting and scanner exceptions are in SECURITY.md.

Architecture

DoppelShield splits into a framework-agnostic engine under src/core/checkurl/ and a thin Next.js adapter, a ports-and-adapters design (ADR-0004): the engine imports nothing from Next.js or React, speaks Web-standard Request and Response, and requires the Node.js runtime for SSRF-safe probing. The one injection seam is the RateLimiter interface, so a shared-store limiter drops in if the service ever scales horizontally. Request ordering is deliberate: rate limiting runs before the body is read, validation before the concurrency gate, and name analysis before the network walk, so rejection stays cheap and homograph findings survive an unreachable host. Production runs as a single ECS Fargate task behind an ALB and a WAF.

The component map, request-lifecycle and detection-pipeline diagrams, and the ADR index are in the architecture overview.

Tech stack

Application: Next.js 16 (App Router), React 19, TypeScript, Node.js 24, Vitest, Docker (Next.js standalone output).

Security and infrastructure: Terraform, AWS ECS Fargate with an ALB and WAF, GitHub Actions with OIDC, CodeQL, Trivy, Sigstore provenance, distroless runtime.

Getting Started

Requires Node.js 24 (see .nvmrc).

npm ci        # Install exact locked dependencies
npm run dev   # Start the dev server at http://localhost:3000

Common scripts:

npm run build       # Production build
npm start           # Run the production build
npm test            # Run the test suite
npm run typecheck   # tsc --noEmit
npm run lint        # Eslint

Usage

Web

Open the app, enter a URL, and submit. DoppelShield decodes the host, traces redirects, and shows the verdict with per-character homoglyph evidence and the redirect chain.

API

curl -X POST http://localhost:3000/api/checkUrl \
  -H 'content-type: application/json' \
  -d '{"url":"https://example.com"}'

A response is a JSON CheckResult, for example:

{
  "apiVersion": 1,
  "ok": true,
  "finalUrl": "https://example.com/",
  "status": 200,
  "redirectChain": [{ "url": "https://example.com/", "status": 200 }],
  "warnings": []
}

Failure modes return ok: false with a stable error.code (invalid_input, rate_limited, unavailable, or unreachable). Blocked and unreachable targets share the single unreachable code so the endpoint never reveals whether an internal host exists.

The full schema, warning codes, outcome matrix, and captured example transcripts are in the API reference.

Configuration

All runtime configuration is via environment variables. Copy the template and adjust as needed:

cp .env.example .env

Two variables deserve attention before a public deploy:

  • NEXT_PUBLIC_SITE_URL is inlined and prerendered at build time (sitemap, robots, canonical and OpenGraph tags). Set it for the build, not at runtime. For Docker, pass --build-arg NEXT_PUBLIC_SITE_URL=https://your.domain.
  • CHECKURL_TRUSTED_IP_HEADER selects the header used to key per-client rate limiting. Behind a proxy or CDN, set it to a header your edge sets or appends: x-forwarded-for behind an AWS ALB (production; the limiter reads the rightmost, ALB-attested entry), x-vercel-forwarded-for on Vercel, cf-connecting-ip on Cloudflare, x-real-ip behind nginx. If it is left unset, all clients share one global rate-limit bucket, so a single visitor can rate-limit everyone.

Every tunable (timeouts, redirect and body limits, allowed ports, rate limits) is documented with its default, accepted bounds, and deployment guidance in the configuration reference; .env.example lists the template values.

Docker

docker build --build-arg NEXT_PUBLIC_SITE_URL=https://your.domain -t doppelshield .
docker run -p 3000:3000 doppelshield

The image is multi-stage: pinned Alpine build stages, and a distroless runtime that serves the Next.js standalone server as a non-root user with no shell or package manager on board (ADR-0009).

Deployment

Production runs as a single container instance behind an edge proxy; the reasoning is recorded in ADR-0007 and the operational procedures (release, deploy, verification, rollback, emergency platform switch) live in the deployment runbook. The topology is realized on AWS as one ECS Fargate task behind an Application Load Balancer and a WAF rate rule scoped to the scan endpoint, defined in Terraform under infra/ and recorded in ADR-0008.

  • Release artifact: version tags publish a scanned, attested image to GHCR that is mirrored by digest to ECR; deploy by digest on any OCI-compatible host. The supply-chain controls are summarized under Security.
  • Health: orchestrator probes and the container HEALTHCHECK target GET /api/health, which sits outside the scan rate limiter and discloses nothing but a status field.
  • Single instance: the rate limiter and concurrency cap are per process and held in memory. Behind multiple replicas the effective limits multiply, and a client can land on different instances. The edge applies the global rate limit; the in-process limiter is defense-in-depth. If horizontal scale is ever needed, a shared-store limiter slots in behind the existing RateLimiter interface.

Testing & CI

npm test                # Run all tests once
npm run test:coverage   # Run with coverage thresholds

The engine (src/core/checkurl/) carries enforced coverage thresholds (statements 85, lines 85, functions 70, branches 68) in vitest.config.ts, and security-sensitive paths (SSRF handling, the redirect walk, request handling) require accompanying tests. CI runs three jobs on every pull request: verify (format, lint, typecheck, tests with coverage, build, and npm audit at the moderate level), infra (terraform fmt and validate on both stacks), and image (a Docker build). CodeQL (security-extended) runs on push, pull request, and a weekly schedule, and release tags add a blocking Trivy image scan gate. The full gate list is in CONTRIBUTING.

Documentation

  • Documentation index: map of all project documentation.
  • Architecture: component map, request lifecycle, detection pipeline, deployment topology.
  • API reference: full schema, outcome matrix, and captured response transcripts.
  • Configuration reference: every tunable with defaults, bounds, and guidance.
  • Threat model: assets, trust boundaries, control mapping, residual risks.
  • Runbook: release, deploy, verification, and rollback procedures.
  • Contributing: setup, quality gates, and conventions.

Roadmap

  • Browser extension: Check the links you browse for homograph domains in place, without pasting manually.

License

This project is licensed under the Apache License, Version 2.0. For the full terms, including the patent grant and redistribution conditions, see LICENSE and NOTICE.

Data Attribution

The brand-target list is derived from the Majestic Million, licensed CC BY 3.0, or an equivalent public domain-popularity ranking, such as Tranco or Cisco Umbrella. See NOTICE for full attribution.

Contact

Joe Maalouf

github.com/joeloufnpmjs.com/~joelouflinkedin.com/in/joelouf

Back to top ↑

About

URL forensics tool that decodes the host, flags look-alike characters, and traces the redirect chain to surface homograph attacks.

Topics

Resources

License

Contributing

Security policy

Stars

0 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors