fix: render insight text as nodes, not innerHTML, and drop stale requests (#1053) - #1058
Merged
Aditya8369 merged 1 commit intoAug 27, 2026
Conversation
…ests (Aditya8369#1053) AnalyticsInsights turned `**bold**` into `<strong>` by writing HTML: dangerouslySetInnerHTML={{ __html: insight.description.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') }} The replace only rewrites the `**` markers, but the whole string is then handed to innerHTML, so every other character in it is interpreted as markup too. `insight.description` is built in aiInsightsService by interpolating the location name, which comes from the geocoder's answer to text the visitor typed — third-party data on a path to innerHTML. A place name containing an `<img src=x onerror=...>` executed in the page, on the origin that holds the community reports, exposure log and leaderboard identity. Adds `renderBoldMarkup`, which splits the string and returns React nodes, so React escapes the text between the markers and inside them the same way it escapes the title, source and confidence right next to it. There is no string of HTML at any point. A missing or non-string description now renders as empty rather than throwing on `.replace`. The insights effect also had no cleanup. `generateAIInsights` reads a year of hourly archive data, so searching Delhi and then Mumbai left two requests in flight and whichever resolved last won — Delhi's insights under Mumbai's heading, with loadingInsights already false so nothing signalled it. A guard flag drops a resolution that is no longer current, which also stops the three setState calls firing after unmount. Fixes Aditya8369#1053
|
@MOHITKOURAV01 is attempting to deploy a commit to the Aditya Mahajan's projects Team on Vercel. A member of the Team first needs to authorize it. |
Thank You for Your Contribution! 🎉Hi @MOHITKOURAV01, Thank you for opening this Pull Request and contributing to our project. We truly appreciate your efforts.
The maintainer @Aditya8369 will review your PR shortly! Happy Contributing! 🚀 |
|
🎉 Your PR just got merged, @MOHITKOURAV01 — thank you for contributing to Pollution Control Hub! Your work is now part of the project. Here's what to do next:
We really appreciate you taking the time. See you in the next PR! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1053
1. The insight body was written as raw HTML
The
replaceonly rewrites the**markers — but the whole string is then handed toinnerHTML, so every other character in it is interpreted as markup too.insight.descriptionis not a constant.aiInsightsServicebuilds it by interpolation:description: `Based on the past year in ${cityName || 'this location'}, **${formatMonth(highestMonth)}** had the highest average pollution ...`cityNamereaches the component fromDashboard, which gets it from the location search — the geocoder's answer to text the visitor typed. Third-party data on a path toinnerHTML. A place name (or a doctored/cached geocoder response) containing<img src=x onerror="...">executed in the page, on the origin that holds community reports, the exposure log and leaderboard identity.React escapes by default, and the rest of this component relies on that:
{insight.title},{insight.source},{insight.confidence}were all already safe. This one line opted out.The fix is
src/utils/boldMarkup.jsx—renderBoldMarkup(text)splits the string and returns React nodes:React escapes the text between the markers, and inside them, the same way it escapes everything else in the component. There is no string of HTML at any point, so there is nothing to get the escaping of wrong.
CommunityHubreached the same conclusion after #497 — its comment now reads "component never uses dangerouslySetInnerHTML".Two details worth naming:
**is left as literal text. Guessing would bold the entire rest of the string, and a lone**in a sentence is far more likely to be punctuation./ginstance keepslastIndexbetween calls, so the second insight would start matching from wherever the first stopped.insight.description.replacealso threw on a missing description. A non-string now renders as empty.2. The insight request was never cancelled
No cleanup, no guard.
generateAIInsightsgoes throughfetchHistoricalData— a year of hourly archive data, slow on a cold cache. Search Delhi, then Mumbai: two requests in flight, and whichever resolves last wins. If Delhi's resolves second, the panel shows Delhi's insights under Mumbai's heading withloadingInsightsalready false, so nothing signals it. It was also the source of the setState-after-unmount warning.A
currentflag in the effect's cleanup drops any resolution that is no longer the live request. The previous city's insights are also cleared when a new one starts, rather than lingering behind the spinner.Verification
src/utils/boldMarkup.test.jsx— 11 tests. The ones that matter are about what does not happen: no element is built from the text,<script>and<img onerror>survive as characters, and markup inside a bold span is escaped too. A test that only checked the bold segments would have passed against theinnerHTMLversion.src/components/AnalyticsInsights.insights.test.jsx— 6 tests at the component level, withaiInsightsServicemocked:cityNameproduces no<img>and no<script>, andwindow.__xssstays undefinedThe 22 existing
AnalyticsInsights.test.jsxtests pass (verified with #1054 applied locally — that file cannot collect onmainbecause #1049 makeshistoricalDataServicethrow on import, which is unrelated to this change). Both new test files pass on plainmain, since mockingaiInsightsServicekeeps the broken module out of the graph entirely.npx eslintis clean on all four changed files.