Skip to content

Add i18n check, URL edge-case coverage, and lint fixes#69

Open
CodewithEvilxd wants to merge 4 commits intomrmps:mainfrom
CodewithEvilxd:main
Open

Add i18n check, URL edge-case coverage, and lint fixes#69
CodewithEvilxd wants to merge 4 commits intomrmps:mainfrom
CodewithEvilxd:main

Conversation

@CodewithEvilxd
Copy link

@CodewithEvilxd CodewithEvilxd commented Feb 13, 2026

What

  • Added check-i18n script to verify locale key consistency.
  • Added URL validation edge-case tests (IPv6, IPv4-mapped, metadata).
  • Hardened IPv4-mapped IPv6 private detection.
  • Allowed admin route tests to accept 401 when auth enabled.
  • Silenced known Next.js <img> lint warnings in OG + lightbox.
  • Renamed unused adProvider var in gravity route.

Why

  • Prevent i18n drift and improve SSRF/URL validation coverage.
  • Keep tests stable across auth environments.

Testing

  • bun run check-i18n
  • bun test

Open with Devin

Summary by CodeRabbit

  • New Features

    • Open Graph image handling now sanitizes and omits unsafe or private-image URLs.
  • Improvements

    • Expanded URL validation to cover additional IPv6/IPv4-mapped formats and edge cases.
    • Exposed validation helpers for reuse across server code.
    • Added an automated i18n shape checker to validate locale message files.
  • Tests

    • Added URL edge-case tests and relaxed/augmented several tests to account for auth and timing variations.
  • Chores

    • Minor lint/noise-suppression edits to image rendering paths.

@vercel
Copy link

vercel bot commented Feb 13, 2026

@CodewithEvilxd is attempting to deploy a commit to the Explainer Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

📝 Walkthrough

Walkthrough

Adds SSRF protection for OG article images by validating and clearing articleImage when URLs are invalid, private, or blocked; exports isPrivateIP and BLOCKED_HOSTNAMES with IPv4-mapped IPv6 handling and tests; adds an i18n shape-check script; inserts eslint disables for img; minor test and route tweaks.

Changes

Cohort / File(s) Summary
OG image handling
app/api/og/route.tsx
Validates parsed articleImage URL (scheme, blocked hostnames, private IPs); clears image when invalid/private; added inline eslint directive near img.
UI image lint exception
components/ui/image-lightbox.tsx
Inserted inline eslint-disable-next-line before img element; no runtime behavior change.
URL validation & tests
lib/validation/url.ts, lib/validation/url.test.ts
Exported isPrivateIP and BLOCKED_HOSTNAMES; extended isPrivateIP to handle IPv4‑mapped IPv6 forms (hex/hextet and expanded) and added edge-case IPv6/IPv4-mapped tests and cloud metadata blocking tests.
i18n tooling
scripts/check-i18n.ts, package.json
Added scripts/check-i18n.ts to compare locale JSON shapes to en.json; added check-i18n script entry.
Tests & route minor edits
server/index.test.ts, server/routes/gravity.ts
Expanded some test status expectations to include 401 and added per-test timeouts; renamed adProvider destructure to _adProvider to mark it unused.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant OGRoute as OG Route
    participant Validator as Validator (isPrivateIP / BLOCKED_HOSTNAMES)
    participant Remote as Image Host

    Client->>OGRoute: Request with articleImage URL
    OGRoute->>Validator: Parse URL, check scheme, hostname, private/IP mapping
    alt Invalid or blocked
        Validator-->>OGRoute: blocked/invalid
        OGRoute-->>Client: Render OG without external image
    else Allowed
        Validator-->>OGRoute: allowed
        OGRoute->>Remote: (optional) fetch or reference image
        Remote-->>OGRoute: image data / response
        OGRoute-->>Client: Render OG with image
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mrmps

Poem

🐇 I hop through hostnames, sniff out hidden nooks,
I clear shady URLs and tidy up the books.
I map IPv6 with a nimble little spin,
I count locale keys till every coin fits in,
Hooray — the rabbit hops, the patch is snug and quick.

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (8 files):

⚔️ app/api/og/route.tsx (content)
⚔️ components/ui/image-lightbox.tsx (content)
⚔️ lib/validation/url.test.ts (content)
⚔️ lib/validation/url.ts (content)
⚔️ lib/zeroclick.ts (content)
⚔️ package.json (content)
⚔️ server/index.test.ts (content)
⚔️ server/routes/gravity.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main changes: adding an i18n check script, expanding URL edge-case test coverage (IPv6/IPv4-mapped scenarios), and applying lint fixes (ESLint directives for img elements and variable renaming).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch main
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
lib/validation/url.ts (1)

106-113: lower.includes("ffff:") is overly broad — could match non-IPv4-mapped IPv6 addresses.

This condition matches any IPv6 containing ffff: anywhere (e.g., abcd:ffff:10.0.0.1), not just the canonical 0:0:0:0:0:ffff: prefix. In practice this errs on the side of caution (false positives only block when the extracted IPv4 is private), so it's safe from a security standpoint, but it's semantically imprecise.

A tighter check could verify the prefix is all-zeros before ffff::

♻️ Suggested tightening
-    // Handle expanded IPv4-mapped IPv6 forms (e.g., 0:0:0:0:0:ffff:10.0.0.1)
-    if (lower.includes("ffff:")) {
+    // Handle expanded IPv4-mapped IPv6 forms (e.g., 0:0:0:0:0:ffff:10.0.0.1)
+    const expandedMappedMatch = lower.match(
+      /^(?:0+:){5}ffff:(\d{1,3}(?:\.\d{1,3}){3})$/
+    );
+    if (expandedMappedMatch) {
+      return isPrivateIP(expandedMappedMatch[1]);
+    }
+    // Fallback: any remaining ffff: with trailing dotted-quad
+    if (lower.includes("ffff:")) {
       const lastColon = lower.lastIndexOf(":");
       const tail = lastColon >= 0 ? lower.slice(lastColon + 1) : "";
       if (tail.includes(".") && /^\d{1,3}(?:\.\d{1,3}){3}$/.test(tail)) {
         return isPrivateIP(tail);
       }
     }
app/api/og/route.tsx (1)

30-58: The apiBaseUrl fetch is also susceptible to SSRF if NEXT_PUBLIC_URL is tampered with.

If NEXT_PUBLIC_URL were ever set to an internal address, the fetch on Line 37 would hit an internal endpoint. Currently this is mitigated by the fallback to "https://smry.ai" and the fact that NEXT_PUBLIC_URL is a build-time env var, but it's worth noting as a defense-in-depth consideration. No action required now.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 13, 2026

Greptile Summary

This PR strengthens SSRF protection by detecting sophisticated IPv4-mapped IPv6 bypass attempts and applies this validation to the OG image route. It also adds a new i18n validation script to prevent locale drift and improves test stability.

  • Enhanced isPrivateIP to detect IPv4-mapped IPv6 in hex hextet format (::ffff:a00:1) and expanded form (0:0:0:0:0:ffff:10.0.0.1)
  • Applied SSRF protection to OG route's articleImage parameter, blocking private IPs and non-http/https protocols
  • Added check-i18n script to validate locale file structure consistency against messages/en.json
  • Increased test timeouts for slow external service tests and relaxed admin route assertions to handle auth scenarios
  • Minor lint fixes: silenced Next.js <img> warnings in OG/lightbox, renamed unused adProvider param

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • All changes are defensive security improvements, test stability fixes, and tooling additions. The SSRF protection logic is comprehensive and well-tested with 34 passing tests. The i18n check script is straightforward validation logic. No breaking changes or risky refactoring.
  • No files require special attention

Important Files Changed

Filename Overview
lib/validation/url.ts Enhanced SSRF protection with comprehensive IPv4-mapped IPv6 detection (hex hextet and expanded forms), exported utilities for reuse
lib/validation/url.test.ts Added edge-case tests for IPv6, IPv4-mapped addresses, userinfo, and cloud metadata endpoints - comprehensive coverage
app/api/og/route.tsx Added SSRF protection for article images by validating URLs against private IPs and blocked hostnames, with protocol restrictions
scripts/check-i18n.ts New script validates locale file structure matches en.json baseline - prevents i18n drift

Flowchart

flowchart TD
    A[User Input URL] --> B[normalizeUrl]
    B --> C{Protocol Check}
    C -->|Valid http/https| D{Hostname Check}
    C -->|Invalid| E[Reject]
    D --> F[isPrivateIP]
    F --> G{IPv4?}
    G -->|Yes| H[Check Private Ranges<br/>10.x, 172.16-31.x, 192.168.x<br/>127.x, 169.254.x, 0.x]
    G -->|No| I{IPv6?}
    I -->|Yes| J[Check IPv6 Ranges]
    J --> K[Loopback ::1]
    J --> L[Link-local fe80:]
    J --> M[Unique local fc/fd]
    J --> N[IPv4-mapped ::ffff:x.x.x.x]
    J --> O[IPv4-mapped hex ::ffff:a00:1]
    J --> P[Expanded IPv4-mapped<br/>0:0:0:0:0:ffff:x.x.x.x]
    N --> Q[Recursively check<br/>mapped IPv4]
    O --> Q
    P --> Q
    D --> R{In BLOCKED_HOSTNAMES?}
    R -->|Yes| E
    R -->|No| S{Private IP?}
    H -->|Private| E
    Q -->|Private| E
    K --> E
    L --> E
    M --> E
    S -->|No| T[Allow URL]
    S -->|Yes| E
    
    U[OG Route Image] --> V{articleImage exists?}
    V -->|Yes| W[Parse URL]
    W --> X{Protocol http/https?}
    X -->|No| Y[Clear image]
    X -->|Yes| Z[Check with isPrivateIP<br/>and BLOCKED_HOSTNAMES]
    Z -->|Blocked| Y
    Z -->|Safe| AA[Render image]
    V -->|No| AB[No image]
Loading

Last reviewed commit: bd240f8

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/routes/gravity.ts (1)

153-159: ⚠️ Potential issue | 🟡 Minor

Stale comment: _adProvider is not actually logged to ClickHouse.

Line 158 states "adProvider from client is used only for ClickHouse logging", but _adProvider is never passed to trackAdEvent (lines 172–192). The comment is misleading — either remove it or, if the intent is to log it, actually pass it as a field.

Suggested comment fix
       const { type, sessionId, hostname, brandName, adTitle, adText, clickUrl, impUrl, cta, favicon, deviceType, os, browser, adProvider: _adProvider } = body;

       // Derive provider from impUrl prefix only — never trust client-sent adProvider
       // for forwarding decisions (prevents spoofing to skip Gravity billing)
       const isZeroClick = impUrl?.startsWith("zeroclick://") ?? false;
-      // adProvider from client is used only for ClickHouse logging, not forwarding logic
+      // adProvider from client is accepted but intentionally unused
       const provider = isZeroClick ? "zeroclick" : "gravity";
app/api/og/route.tsx (1)

124-142: ⚠️ Potential issue | 🟡 Minor

ESLint suppression is correct — next/image is unsupported in ImageResponse (Satori).

However, there's an unaddressed SSRF vulnerability: articleImage originates from unvalidated user input (?image=... query parameter or fetched article metadata) and Satori will fetch it server-side during image generation. An attacker could pass ?image=http://169.254.169.254/latest/meta-data/ or other private IP addresses. Apply the existing isPrivateIP() validation from lib/validation/url.ts to articleImage before rendering to prevent this attack vector.

🤖 Fix all issues with AI agents
In `@lib/validation/url.test.ts`:
- Around line 80-89: Add unit tests to cover the new IPv4-mapped hex-hextet and
expanded IPv6 parsing branches that currently lack coverage: add tests invoking
normalizeUrl with "[::ffff:a00:1]" (hex-hextet private) and "[::ffff:808:808]"
(hex-hextet public) and with the expanded form "[0:0:0:0:0:ffff:10.0.0.1]" to
assert the private addresses throw "Cannot access internal or private network
addresses." and the public hex-hextet returns the normalized URL; place these in
lib/validation/url.test.ts alongside the existing IPv4-mapped tests so the code
paths in url.ts (the hex-hextet branch and the expanded form branch) are
exercised.
🧹 Nitpick comments (3)
scripts/check-i18n.ts (2)

86-97: Static analysis: forEach callbacks should not return a value.

Biome flags lines 88, 92, and 96 because the arrow-function shorthand implicitly returns the result of console.log(). Use block bodies to silence the lint errors.

Proposed fix
     if (missing.length) {
       console.log(`  Missing (${missing.length})`);
-      missing.forEach((key) => console.log(`    - ${key}`));
+      missing.forEach((key) => { console.log(`    - ${key}`); });
     }
     if (extra.length) {
       console.log(`  Extra (${extra.length})`);
-      extra.forEach((key) => console.log(`    - ${key}`));
+      extra.forEach((key) => { console.log(`    - ${key}`); });
     }
     if (typeMismatches.length) {
       console.log(`  Type mismatches (${typeMismatches.length})`);
-      typeMismatches.forEach((key) => console.log(`    - ${key}`));
+      typeMismatches.forEach((key) => { console.log(`    - ${key}`); });
     }

60-66: Consider wrapping the base file read in a try/catch with a user-friendly error.

If messages/en.json doesn't exist (e.g., running from wrong directory), readFileSync will throw a raw ENOENT error. A small guard would improve DX.

Proposed improvement
 const projectRoot = process.cwd();
 const messagesDir = join(projectRoot, "messages");
 const baseFile = join(messagesDir, "en.json");

+import { existsSync } from "fs";
+if (!existsSync(baseFile)) {
+  console.error(`Base locale file not found: ${baseFile}`);
+  process.exit(1);
+}
+
 const baseJson = readJson(baseFile);
lib/validation/url.ts (1)

103-110: Overly broad includes("ffff:") match may false-positive on non-mapped IPv6.

Any IPv6 address containing the substring "ffff:" will enter this branch — e.g. 2001:db8:0:0:0:ffff:192.168.1.1 is a legitimate (non-mapped) IPv6 address whose embedded IPv4 tail happens to be private. This fails safely (over-blocks rather than under-blocks), but it could be tightened to only match the canonical mapped prefix pattern:

Tighter match for expanded IPv4-mapped forms
-    // Handle expanded IPv4-mapped IPv6 forms (e.g., 0:0:0:0:0:ffff:10.0.0.1)
-    if (lower.includes("ffff:")) {
-      const lastColon = lower.lastIndexOf(":");
-      const tail = lastColon >= 0 ? lower.slice(lastColon + 1) : "";
-      if (tail.includes(".") && /^\d{1,3}(?:\.\d{1,3}){3}$/.test(tail)) {
-        return isPrivateIP(tail);
-      }
-    }
+    // Handle expanded IPv4-mapped IPv6 forms (e.g., 0:0:0:0:0:ffff:10.0.0.1)
+    const expandedMapped = lower.match(
+      /^(?:0+:){5}(?:0*f{4}):(\d{1,3}(?:\.\d{1,3}){3})$/
+    );
+    if (expandedMapped) {
+      return isPrivateIP(expandedMapped[1]);
+    }

…verage

- Add SSRF protection to OG image route by validating articleImage URLs
- Export isPrivateIP function for use in OG route
- Fix misleading comment in gravity.ts about adProvider usage
- Fix forEach lint warnings in check-i18n.ts with explicit block bodies
- Add existsSync check for base locale file in check-i18n.ts
- Add missing hex-hextet public IPv6 test case
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/api/og/route.tsx`:
- Around line 70-85: The SSRF check in the articleImage validation only calls
isPrivateIP(imageHost) and misses internal non-numeric hostnames; update the
validation in route.tsx to also (1) ensure the URL.protocol is either "http:" or
"https:" and (2) check the imageUrl.hostname against the blocked hostnames list
(BLOCKED_HOSTNAMES) exported from url.ts (or import/export it if not yet
exported) and clear articleImage when the hostname matches; keep the existing
try/catch and numeric-IP check (isPrivateIP) and combine these checks before
accepting the image URL.
🧹 Nitpick comments (3)
scripts/check-i18n.ts (2)

1-17: Wrap readJson in a try/catch for friendlier errors on malformed locale files.

If a locale JSON file contains a syntax error, the script will crash with a raw SyntaxError and stack trace instead of a clear, actionable message identifying which file is broken.

♻️ Suggested improvement
 function readJson(filePath: string): unknown {
-  const raw = readFileSync(filePath, "utf-8");
-  return JSON.parse(raw);
+  const raw = readFileSync(filePath, "utf-8");
+  try {
+    return JSON.parse(raw);
+  } catch (err) {
+    console.error(`Failed to parse JSON: ${filePath}`);
+    throw err;
+  }
 }

19-35: collectShape does not recurse into array elements — intentional?

Arrays are recorded as a leaf "array" type, so structural differences inside array items (e.g., an array of objects with translatable keys) won't be detected. If your locale files only use arrays for simple value lists this is fine; just calling it out in case nested-object arrays are expected.

lib/validation/url.ts (1)

103-110: The "ffff:" substring check is intentionally broad — consider tightening.

lower.includes("ffff:") will match any IPv6 address containing that substring, not just IPv4-mapped forms. The inner dotted-decimal guard on Line 107 prevents most false positives, but a tighter prefix check (e.g., matching the full 0:0:0:0:0:ffff: pattern or ending with :ffff:<ipv4>) would be more precise. This is non-urgent since over-blocking is safer for SSRF prevention.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

- Export BLOCKED_HOSTNAMES from url.ts for reuse
- Add protocol validation (only allow http/https) in OG image route
- Check articleImage hostname against BLOCKED_HOSTNAMES
- Prevents SSRF via internal DNS names like metadata.google.internal

Addresses CodeRabbit feedback on incomplete SSRF protection
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant