-
Notifications
You must be signed in to change notification settings - Fork 0
feat(seo): add bot detection for dynamic og:image tags #3171
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,9 @@ | |||||||||||||||||||||||||||||
| """SEO endpoints (sitemap).""" | |||||||||||||||||||||||||||||
| """SEO endpoints (sitemap, bot-optimized pages).""" | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| import html | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| from fastapi import APIRouter, Depends | |||||||||||||||||||||||||||||
| from fastapi.responses import Response | |||||||||||||||||||||||||||||
| from fastapi import APIRouter, Depends, HTTPException | |||||||||||||||||||||||||||||
| from fastapi.responses import HTMLResponse, Response | |||||||||||||||||||||||||||||
| from sqlalchemy.ext.asyncio import AsyncSession | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| from api.cache import cache_key, get_cache, set_cache | |||||||||||||||||||||||||||||
|
|
@@ -14,6 +14,34 @@ | ||||||||||||||||||||||||||||
| router = APIRouter(tags=["seo"]) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Minimal HTML template for social media bots (meta tags are what matters) | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE = """<!DOCTYPE html> | |||||||||||||||||||||||||||||
| <html lang="en"> | |||||||||||||||||||||||||||||
| <head> | |||||||||||||||||||||||||||||
| <meta charset="UTF-8" /> | |||||||||||||||||||||||||||||
| <title>{title}</title> | |||||||||||||||||||||||||||||
| <meta name="description" content="{description}" /> | |||||||||||||||||||||||||||||
| <meta property="og:title" content="{title}" /> | |||||||||||||||||||||||||||||
| <meta property="og:description" content="{description}" /> | |||||||||||||||||||||||||||||
| <meta property="og:image" content="{image}" /> | |||||||||||||||||||||||||||||
| <meta property="og:url" content="{url}" /> | |||||||||||||||||||||||||||||
| <meta property="og:type" content="website" /> | |||||||||||||||||||||||||||||
| <meta property="og:site_name" content="pyplots.ai" /> | |||||||||||||||||||||||||||||
| <meta name="twitter:card" content="summary_large_image" /> | |||||||||||||||||||||||||||||
| <meta name="twitter:title" content="{title}" /> | |||||||||||||||||||||||||||||
| <meta name="twitter:description" content="{description}" /> | |||||||||||||||||||||||||||||
| <meta name="twitter:image" content="{image}" /> | |||||||||||||||||||||||||||||
| <link rel="canonical" href="{url}" /> | |||||||||||||||||||||||||||||
| </head> | |||||||||||||||||||||||||||||
| <body><h1>{title}</h1><p>{description}</p></body> | |||||||||||||||||||||||||||||
| </html>""" | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| DEFAULT_IMAGE = "https://pyplots.ai/og-image.png" | |||||||||||||||||||||||||||||
| DEFAULT_DESCRIPTION = ( | |||||||||||||||||||||||||||||
| "Library-agnostic, AI-powered Python plotting examples. Automatically generated, tested, and maintained." | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @router.get("/sitemap.xml") | |||||||||||||||||||||||||||||
| async def get_sitemap(db: AsyncSession | None = Depends(optional_db)): | |||||||||||||||||||||||||||||
| """ | |||||||||||||||||||||||||||||
|
|
@@ -53,3 +81,95 @@ async def get_sitemap(db: AsyncSession | None = Depends(optional_db)): | ||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| set_cache(key, xml) | |||||||||||||||||||||||||||||
| return Response(content=xml, media_type="application/xml") | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # ============================================================================= | |||||||||||||||||||||||||||||
| # Bot SEO Proxy Endpoints | |||||||||||||||||||||||||||||
| # These endpoints serve HTML with correct meta tags for social media bots. | |||||||||||||||||||||||||||||
| # nginx proxies bot requests here based on User-Agent detection. | |||||||||||||||||||||||||||||
| # ============================================================================= | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @router.get("/seo-proxy/") | |||||||||||||||||||||||||||||
| async def seo_home(): | |||||||||||||||||||||||||||||
| """Bot-optimized home page with correct og:tags.""" | |||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title="pyplots.ai", description=DEFAULT_DESCRIPTION, image=DEFAULT_IMAGE, url="https://pyplots.ai/" | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @router.get("/seo-proxy/catalog") | |||||||||||||||||||||||||||||
| async def seo_catalog(): | |||||||||||||||||||||||||||||
| """Bot-optimized catalog page with correct og:tags.""" | |||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title="Catalog | pyplots.ai", | |||||||||||||||||||||||||||||
| description="Browse all Python plotting specifications alphabetically. Find matplotlib, seaborn, plotly, bokeh, altair examples.", | |||||||||||||||||||||||||||||
| image=DEFAULT_IMAGE, | |||||||||||||||||||||||||||||
| url="https://pyplots.ai/catalog", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @router.get("/seo-proxy/{spec_id}") | |||||||||||||||||||||||||||||
| async def seo_spec_overview(spec_id: str, db: AsyncSession | None = Depends(optional_db)): | |||||||||||||||||||||||||||||
| """Bot-optimized spec overview page with correct og:tags.""" | |||||||||||||||||||||||||||||
| if db is None: | |||||||||||||||||||||||||||||
| # Fallback when DB unavailable | |||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title=f"{spec_id} | pyplots.ai", | |||||||||||||||||||||||||||||
| description=DEFAULT_DESCRIPTION, | |||||||||||||||||||||||||||||
| image=DEFAULT_IMAGE, | |||||||||||||||||||||||||||||
| url=f"https://pyplots.ai/{html.escape(spec_id)}", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
Comment on lines
+122
to
+127
Check warningCode scanning / CodeQL Reflected server-side cross-site scripting Medium
Cross-site scripting vulnerability due to a
user-provided value Error loading related location Loading
Copilot AutofixAI 4 months ago In general, to fix reflected server-side XSS, every user-controlled value inserted into an HTML document must be properly escaped for the context in which it appears (HTML body, attribute, URL, etc.). In this file, all uses of The single best minimal fix is to escape
Suggested changeset
1
api/routers/seo.py
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Refresh and try again.
|
|||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| repo = SpecRepository(db) | |||||||||||||||||||||||||||||
| spec = await repo.get_by_id(spec_id) | |||||||||||||||||||||||||||||
| if not spec: | |||||||||||||||||||||||||||||
| raise HTTPException(status_code=404, detail="Spec not found") | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title=f"{html.escape(spec.title)} | pyplots.ai", | |||||||||||||||||||||||||||||
| description=html.escape(spec.description or DEFAULT_DESCRIPTION), | |||||||||||||||||||||||||||||
| image=DEFAULT_IMAGE, | |||||||||||||||||||||||||||||
| url=f"https://pyplots.ai/{html.escape(spec_id)}", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| @router.get("/seo-proxy/{spec_id}/{library}") | |||||||||||||||||||||||||||||
| async def seo_spec_implementation(spec_id: str, library: str, db: AsyncSession | None = Depends(optional_db)): | |||||||||||||||||||||||||||||
| """Bot-optimized spec implementation page with dynamic og:image from preview_url.""" | |||||||||||||||||||||||||||||
| if db is None: | |||||||||||||||||||||||||||||
| # Fallback when DB unavailable | |||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title=f"{html.escape(spec_id)} - {html.escape(library)} | pyplots.ai", | |||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||
| description=DEFAULT_DESCRIPTION, | |||||||||||||||||||||||||||||
| image=DEFAULT_IMAGE, | |||||||||||||||||||||||||||||
| url=f"https://pyplots.ai/{html.escape(spec_id)}/{html.escape(library)}", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
Comment on lines
+151
to
+156
Check warningCode scanning / CodeQL Reflected server-side cross-site scripting Medium
Cross-site scripting vulnerability due to a user-provided value.
Cross-site scripting vulnerability due to a user-provided value.
Copilot AutofixAI 4 months ago In general, to fix reflected server-side XSS in this endpoint, all user-controlled values ( Concretely, in
Suggested changeset
1
api/routers/seo.py
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Refresh and try again.
|
|||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| repo = SpecRepository(db) | |||||||||||||||||||||||||||||
| spec = await repo.get_by_id(spec_id) | |||||||||||||||||||||||||||||
| if not spec: | |||||||||||||||||||||||||||||
| raise HTTPException(status_code=404, detail="Spec not found") | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| # Find the implementation for this library | |||||||||||||||||||||||||||||
| impl = next((i for i in spec.impls if i.library_id == library), None) | |||||||||||||||||||||||||||||
| image = impl.preview_url if impl and impl.preview_url else DEFAULT_IMAGE | |||||||||||||||||||||||||||||
|
|
|||||||||||||||||||||||||||||
| return HTMLResponse( | |||||||||||||||||||||||||||||
| BOT_HTML_TEMPLATE.format( | |||||||||||||||||||||||||||||
| title=f"{html.escape(spec.title)} - {html.escape(library)} | pyplots.ai", | |||||||||||||||||||||||||||||
| description=html.escape(spec.description or DEFAULT_DESCRIPTION), | |||||||||||||||||||||||||||||
| image=html.escape(image, quote=True), | |||||||||||||||||||||||||||||
| url=f"https://pyplots.ai/{html.escape(spec_id)}/{html.escape(library)}", | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
| ) | |||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec_id in the title is not HTML-escaped, which could lead to XSS if a malicious spec_id is provided. All user-controlled inputs should be escaped before insertion into HTML. The url parameter already escapes it correctly, but the title needs the same treatment.