Skip to content

Remediate project audit findings - #342

Open
baixiangcpp wants to merge 1 commit into
mainfrom
fix/project-audit-remediation
Open

Remediate project audit findings#342
baixiangcpp wants to merge 1 commit into
mainfrom
fix/project-audit-remediation

Conversation

@baixiangcpp

Copy link
Copy Markdown
Owner

Summary

  • Ticket: Project-wide audit remediation; no linked issue.
  • Scope: Correctness and injection fixes across audited tools, explicit external-image consent, privacy-safe Service Worker caching, stale async result protection, production dependency updates, cross-platform build/start scripts, and compressed i18n payloads.
  • Risk: Medium. The change is cross-cutting across tool logic, privacy metadata, generated registries, dependencies, and the offline shell. Full unit, build, browser smoke, PWA, metadata, localization, and performance gates mitigate the risk. The production audit is clear; the full audit still contains development-tool-only advisories whose suggested fixes require breaking upgrades or downgrades.

Change Type

  • Feature
  • Bugfix
  • Refactor
  • Docs
  • CI/CD
  • Tests / Guards
  • Tool registry / generated data

Release Checklist

  • User-facing behavior and acceptance criteria are covered
  • i18n keys updated across all supported locales, if applicable
  • Tool manifest, metadata, related tools, and generated registry files updated, if applicable
  • Tests or guards updated for changed behavior or structure
  • Desktop, mobile, keyboard, and focus states checked for UI changes
  • No secrets, private payloads, local reports, or process files committed

Commands Run

  • npm run check:audit:prod
  • npm run generate:tool-index
  • npm run check:tool-index
  • npm run generate:client-tool-lookup
  • npm run check:client-tool-lookup
  • npm run lint
  • npm run test
  • npm run check:types
  • npm run validate
  • npm run build
  • npm run test:e2e:smoke
  • npm run test:e2e:pwa

Route Metadata / Export Checklist

  • Canonical/hreflang checks pass
  • Content template sections present and localized when required
  • Static export post-processing checks pass

Validation Evidence

  • Commands run: Full unit suite, lint, TypeScript, production audit, validation/build pipeline, Smoke, PWA, and a direct npm start clean-route probe.
  • Key output: 327 test files passed with 1,771 tests passed and 3 skipped; 1,534 static pages generated; all 7 performance budgets passed; production audit reports 0 vulnerabilities; Smoke and fresh-install offline PWA journeys passed.
  • Performance: root HTML fell from about 328.6 KiB to 257.3 KiB; the static export is about 1.263 GB, roughly 27% below the prior 1.73 GB baseline.
  • Screenshots / recordings: Automated Playwright first-load, mobile, accessibility, navigation, QR download, and PWA journeys passed; no manual recording attached.

Rollback Plan

  • What to revert: Revert commit 143b216 as one unit, then rebuild so generated registries and the injected Service Worker build ID stay aligned.
  • User impact: Rollback restores the previous parser behavior, preview/network policy, dependency graph, offline cache behavior, and larger translation payloads.

Harden tool parsing, async result handling, external previews, and service worker cache privacy.

Update production dependencies, deployment scripts, generated metadata, and compressed translation payloads with regression coverage.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying byteflow with  Cloudflare Pages  Cloudflare Pages

Latest commit: 143b216
Status: ✅  Deploy successful!
Preview URL: https://d34a9f33.byteflow-c58.pages.dev
Branch Preview URL: https://fix-project-audit-remediatio.byteflow-c58.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 143b216245

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import { existsSync } from "node:fs";
import { createServer } from "node:http";
import path from "node:path";
import serveHandler from "serve-handler";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move the start server dependency into production dependencies

When a self-hosting or container deployment installs with npm ci --omit=dev (the checked npm ci --help lists --omit <dev|optional|peer>), this import throws ERR_MODULE_NOT_FOUND before the server starts because serve-handler remains only in devDependencies. Since npm start now always executes this file, move serve-handler into dependencies or avoid requiring a development-only package at runtime.

Useful? React with 👍 / 👎.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[Blocking / High] Production-only installs cannot run npm start

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9. This expands the existing inline P1 review at #342 (comment) with the deployment and test details needed to close it.

Affected code

  • package.json:61 makes node scripts/deployment/serve-export.js the package's production start command.
  • scripts/deployment/serve-export.js:6 unconditionally imports serve-handler before validating the output directory or starting the HTTP server.
  • package.json:209 still declares serve-handler under devDependencies, not dependencies.
  • package-lock.json:12226-12230 records node_modules/serve-handler with dev: true.
  • docs/deployment/self-hosting.md:17 now explicitly recommends npm start as the production-like clean-URL server, so this is part of the documented deployment path rather than an internal development helper.

Reproduction / evidence

From the current checkout:

$ npm ls --omit=dev serve-handler --depth=0
byteflow@0.1.0 .../byteflow.tools
`-- (empty)

exit code: 1

The equivalent clean deployment sequence is:

npm ci --omit=dev
npm start

Because npm omits serve-handler and its dependency tree, Node evaluates the static import at process startup and fails before server.listen(...):

Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'serve-handler'

A normal developer checkout and the existing smoke job do not expose this because they install development dependencies before invoking the server.

Expected behavior

A documented runtime command must work after a standard production-only install. A static export copied into a runtime/container stage with npm ci --omit=dev should be able to execute npm start and serve /en plus extensionless routes such as /en/json-formatter.

Actual behavior and impact

Self-hosted, container, and multi-stage deployments that prune development dependencies fail immediately. Health checks never become ready, clean-URL behavior cannot be exercised, and the deployment rolls back or crash-loops even though the build and current CI are green.

Root cause

The PR converted npm start into a real runtime entry point but left its unconditional runtime import in the development dependency set. The package manifest, lockfile classification, documentation, and runtime contract are therefore inconsistent.

Recommended fix

Move serve-handler from devDependencies to dependencies and regenerate package-lock.json, verifying that the root dependency and node_modules/serve-handler are no longer marked development-only. If keeping it out of production dependencies is intentional, the start script must instead use a runtime implementation that depends only on production packages / Node built-ins, and the self-hosting documentation must match that choice.

Regression coverage

Add a deployment smoke check that uses a disposable staging directory or container:

  1. Install with npm ci --omit=dev.
  2. Copy/use the built out/ directory.
  3. Start npm start on an ephemeral port.
  4. Assert /en and /en/json-formatter return 200 and that .html canonicalization behaves as intended.
  5. Terminate the server cleanly.

A lightweight manifest assertion that every statically imported module reachable from scripts/deployment/serve-export.js is a production dependency would provide an earlier failure, but it should supplement rather than replace the production-install smoke test.

Merge assessment: blocking. The PR currently advertises and validates a deployment command that fails in a conventional production install.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[High] Returning from Decode mode leaves an enabled PNG action attached to a blank canvas

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 with a fresh Chromium run against this PR's Cloudflare preview.

Affected code

  • src/features/tools/qr-code-generator/page.tsx:135-149: the render effect depends only on renderToCanvas; mode is not a dependency.
  • src/features/tools/qr-code-generator/page.tsx:388-399: switching modes conditionally unmounts and remounts QrGeneratePanel and therefore its <canvas>.
  • src/features/tools/qr-code-generator/panels.tsx:218-221: the canvas exists only while Generate mode is mounted and the text is non-empty.
  • src/features/tools/qr-code-generator/page.tsx:186-192: PNG export reads the currently mounted canvasRef.
  • src/features/tools/qr-code-generator/page.tsx:315 and :329: PNG and Copy are enabled from the old dataUrl, which is not invalidated by a mode switch.

User reproduction

  1. Open /en/qr-code-generator on the PR preview.
  2. Enter any non-empty payload and wait for the generated QR preview.
  3. Click Decode image.
  4. Click Generate without changing any QR setting.
  5. Observe that the preview is blank while the PNG and Copy actions remain enabled.
  6. Click PNG. The exported image comes from the newly mounted blank canvas rather than the QR that was previously generated.

Browser evidence

A fresh headless Chromium run against https://d34a9f33.byteflow-c58.pages.dev/en/qr-code-generator measured the canvas before and after the two tab clicks:

{
  before: {
    width: 256,
    height: 256,
    dataUrlLength: 5322,
    nonTransparentPixels: 65536
  },
  after: {
    width: 300,
    height: 150,
    dataUrlLength: 2118,
    nonTransparentPixels: 0
  }
}

After returning to Generate mode, both PNG and Copy reported disabled: false. The 300 x 150 dimensions are the browser's default dimensions for a newly created canvas, and the pixel inspection confirms that every pixel is transparent.

Expected behavior

Returning to Generate mode should immediately show the QR that corresponds to the current form state. Export/copy actions must refer to that same rendered artifact; they must be disabled if no current artifact exists.

Actual behavior and impact

The visible canvas is replaced, but the state variable used to enable actions survives. This creates two inconsistent artifacts:

  • PNG serializes the new blank canvas.
  • Copy still uses the old dataUrl, so it can copy an image that is not visible and is no longer tied to the mounted canvas.

The operation reports success even though the PNG is unusable, making this a silent data-integrity failure rather than a cosmetic preview issue.

Root cause

Changing mode does not change any dependency captured by renderToCanvas, so the effect does not rerun when a new canvas node is mounted. React assigns canvasRef.current to the new blank element, while dataUrl remains truthy from the previous render.

This mode/remount gap appears to predate the offscreen-render change, but it remains in a tool touched by this project-wide correctness audit and interacts directly with the new render lifecycle.

Recommended fix

Make the render lifecycle aware of the canvas remount. Viable approaches include:

  • include Generate-mode entry in the render trigger and rerender only when mode === generate;
  • keep the generation canvas mounted while Decode mode is active, if that matches the accessibility/layout design; or
  • use a callback ref / layout-aware render trigger that renders whenever a new visible canvas node is attached.

In all cases, invalidate or gate export state while the visible canvas does not contain the current committed render. Do not rely on a historical dataUrl merely being non-empty.

Regression coverage

Add a component/browser test that:

  1. Generates a non-empty QR and confirms a populated 256 x 256 canvas.
  2. Switches Generate -> Decode -> Generate without changing inputs.
  3. Confirms the remounted canvas is redrawn.
  4. Confirms PNG and Copy refer to the current QR, not a blank canvas or stale data URL.
  5. Ideally inspects canvas pixels or the mocked payload, rather than only checking that a button is enabled.

Merge assessment: blocking for the QR workflow because a normal tab sequence produces a successful-looking but blank export.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[High] PNG and Copy remain enabled for the previous QR while a new asynchronous render is pending

Status: Confirmed by the current state flow on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9. This is separate from the mode-remount bug: it occurs while staying in Generate mode and changing the payload or render options.

Affected code

  • src/features/tools/qr-code-generator/page.tsx:86-133: the PR now renders into an offscreen canvas and commits it to the visible canvas only after all asynchronous work succeeds.
  • src/features/tools/qr-code-generator/page.tsx:135-149: a new request ID is created, but the previous dataUrl is not cleared and there is no isRendering / artifact-version state.
  • src/features/tools/qr-code-generator/page.tsx:130-132: the visible canvas and dataUrl are updated only at the final commit point.
  • src/features/tools/qr-code-generator/page.tsx:186-192 and :233-239: PNG reads the visible canvas and Copy reads dataUrl.
  • src/features/tools/qr-code-generator/page.tsx:315 and :329: both actions are disabled only when dataUrl is empty.

Deterministic reproduction

This is easiest to make deterministic by delaying qrcode.toCanvas(...) or loadImage(...) in a component test:

  1. Render payload A and let it complete. The visible canvas and dataUrl now contain A.
  2. Change the text to payload B, or enable/change a logo whose image decode is delayed.
  3. Keep the B render promise pending.
  4. Before B resolves, click PNG or Copy.

At step 3, the form displays B but the visible canvas and dataUrl still contain A. Since dataUrl is truthy, both actions remain enabled. PNG downloads A and Copy copies A even though the current input says B.

The same window can occur during the lazy QR module load, QR encoding, logo image decoding, or any future asynchronous renderer work. Slow devices and larger logo images make it easier for a user to hit.

Expected behavior

An export action must either:

  • produce the artifact corresponding to the exact current form state, or
  • remain disabled/pending until that artifact has committed.

A stale result must never be presented as a successful export of the current input.

Actual behavior and impact

The request ID correctly prevents an older asynchronous request from overwriting a newer one, but it does not invalidate the last successful artifact when a new request starts. Users can therefore silently distribute a QR code containing the previous URL/text. This is especially risky when the old and new inputs differ only in a token, destination, environment, or campaign identifier: the image looks structurally valid, so the mismatch may not be noticed before publication.

SVG export is computed from current React state while PNG/Copy are based on the last committed raster render, so different export buttons can also produce different payloads during the same pending interval.

Root cause

The PR introduced a two-phase offscreen render (createQrRenderCanvas -> asynchronous render/logo work -> commitQrRenderCanvas) but models only the committed dataUrl, not whether it corresponds to the current render signature. Starting request N+1 increments renderRequestRef, but leaves request N's successful dataUrl and visible canvas eligible for actions.

Recommended fix

Track render validity explicitly. For example:

  • set an isRendering / renderPending flag as soon as a new request starts;
  • associate the committed artifact with a stable signature or request ID derived from all render inputs;
  • enable PNG/Copy only when the committed request ID/signature equals the current request;
  • clear the pending flag only when the latest request commits or fails;
  • on failure, keep actions disabled and surface the render error instead of silently retaining an exportable stale artifact.

Keeping the old preview visible during rendering is acceptable, but it should be visibly pending and must not be exportable as the new result.

Missing test coverage

tests/component/qr-code-generator-logo-upload.test.tsx:172-203 verifies that an older delayed request cannot overwrite a newer request, but it clicks Copy only after the newer request has completed. It does not cover the stale-action interval.

Add tests that:

  1. Complete A.
  2. Start B with a deferred toCanvas or loadImage promise.
  3. Assert PNG and Copy are disabled (or return a pending/failure result) before B commits.
  4. Resolve B and assert both actions now use B.
  5. Reject B and assert A is not silently exportable as B.

Merge assessment: blocking because the UI can successfully export a valid-looking QR for the wrong payload.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[High] The new Crontab pre-validator rejects common expressions that the tool's parser supports

Status: Reproduced on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 by calling both the new validator and the installed cronstrue dependency with the same expressions.

Affected code

  • src/features/tools/crontab-generator/logic.ts:55-84: validateCronField now accepts only *, decimal numbers, decimal ranges, lists, and positive numeric steps.
  • src/features/tools/crontab-generator/logic.ts:93-96: every field must pass that new grammar before the page calls its real parser.
  • src/features/tools/crontab-generator/page.tsx:75-83: validateCronExpression gates cronstrue.toString(...), so anything rejected by the local regex never reaches cronstrue.
  • tests/unit/crontab-generator-logic.test.ts:31-46: the added tests cover arbitrary text/malformed numeric ranges and the narrow accepted subset, but not supported named or special tokens.

Confirmed examples

The current validator returns { ok: false, error: invalid } for every expression below, while cronstrue@3.12.0 successfully produces these descriptions:

Expression cronstrue.toString(...) result Current validator
0 9 * JAN MON At 09:00 AM, only on Monday, only in January rejected
0 0 * * MON-FRI At 12:00 AM, Monday through Friday rejected
0 9 ? * MON At 09:00 AM, only on Monday rejected
0 0 L * * At 12:00 AM, on the last day of the month rejected

Commands used for confirmation:

cronstrue.toString(expression, { throwExceptionOnParseError: true })
validateCronExpression(expression, invalid)

Regression from main

Before this PR, validateNumericTokens range-checked numeric fragments but did not impose a complete token grammar. The expressions above therefore reached cronstrue, which could interpret them. This PR replaces that behavior with:

/^(\*|\d+|\d+-\d+)(?:\/(\d+))?$/

That whitelist excludes:

  • month names such as JAN and JAN-MAR;
  • weekday names such as MON and MON-FRI;
  • ? used for an unspecified day-of-month/day-of-week field;
  • L and related last-day/last-weekday forms;
  • other dialect tokens already understood by the selected parser.

Expected behavior

The generator should accept the cron syntax supported by its documented/installed parser, or explicitly define and communicate a smaller dialect before users enter an expression. Tightening validation must not silently remove previously functioning syntax.

Actual behavior and impact

Common crontab expressions copied from deployment manifests, CI schedules, Kubernetes/Quartz-style configurations, and operational runbooks now show a generic invalid-expression error even though the tool can describe them. Existing saved expressions stop working after this change.

Because the rejection happens before cronstrue, users receive no parser-specific explanation and cannot distinguish an actually malformed expression from syntax that the new regex forgot to model.

Root cause

The local validator is attempting to duplicate a cron grammar with a field-agnostic regular expression. Cron fields have different named aliases and special tokens, and supported dialects are more expressive than numeric ranges. The validator and cronstrue have diverged.

Recommended fix

Prefer one authoritative parser. Let cronstrue perform syntax parsing with throwExceptionOnParseError: true, then add only the supplemental checks that the library demonstrably does not enforce. If early field-specific validation is required, implement a field-aware grammar that intentionally covers the same dialect and aliases as cronstrue; do not apply one numeric-only regex to all fields.

If the product intentionally supports only a strict numeric POSIX subset, that is a behavior/product decision and should be made explicit in the UI/help text. In that case the default examples, error messages, and tests must clearly state the restriction rather than presenting supported expressions as generically invalid.

Regression coverage

Add positive tests for at least:

0 9 * JAN MON
0 0 * * MON-FRI
0 9 ? * MON
0 0 L * *

Keep the new negative tests for */0, banana, 1-2-3, reversed ranges, and out-of-range values. A table-driven test that feeds each accepted expression through both validateCronExpression and cronstrue.toString would prevent the two layers from drifting again.

Merge assessment: blocking for the Crontab tool because the PR introduces a user-visible compatibility regression for valid, commonly used schedules.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[Medium] Service Worker install fetches the same app-shell assets once per language route

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 against the generated out/ pages from the validated build.

Affected code

  • public/sw.js:32-40: APP_SHELL_ASSETS contains seven localized home routes.
  • public/sw.js:205-216: extractAppShellAssets deduplicates URLs only within one HTML document by creating a new local Set per call.
  • public/sw.js:218-225: every extracted URL executes a new fetch, opens a cache, and performs cache.put.
  • public/sw.js:228-243: each route independently extracts and precaches its asset list.
  • public/sw.js:245-248: all seven route jobs run concurrently via Promise.all, with no shared URL set or in-flight promise map.
  • public/sw.js:283-289: any rejection propagates to the install event's waitUntil promise.

Measured build evidence

Parsing the seven generated localized pages with the same src/href and /_next/static/ rules used by the Service Worker produced:

{
  perPage: {
    en: 16,
    zh-CN: 16,
    zh-TW: 16,
    ja: 16,
    ko: 16,
    de: 16,
    fr: 16
  },
  totalPerPageAssets: 112,
  globallyUniqueAssets: 16,
  duplicateFetchInvocations: 96
}

All seven home pages currently reference the same 16 Next.js static assets. The implementation therefore schedules 112 calls to precacheAppShellAsset, although only 16 unique URLs exist. Each shared URL can be fetched and written seven times during one Service Worker install.

Browser HTTP caching or request coalescing may reduce transferred response bodies in some environments, but it does not make the algorithm correct: the Service Worker still creates duplicate fetch promises and duplicate cache.put work, and cache revalidation/coalescing behavior is browser/server dependent.

Expected behavior

Each unique app-shell resource should be requested and cached at most once per Service Worker installation, regardless of how many localized HTML routes reference it.

Actual behavior and impact

  • Installation performs unnecessary network round trips/revalidations and CacheStorage writes.
  • Mobile, high-latency, metered, and self-hosted deployments pay the cost on every new Service Worker version.
  • Concurrent duplicate requests increase load on the origin/CDN during rollout.
  • precacheAppShellAsset throws for every non-2xx response. One transient failure among the duplicated requests rejects its route promise, which rejects the outer Promise.all, which rejects install and prevents the worker from installing. Repeating the same required resource request seven times increases the number of failure opportunities without increasing offline coverage.

The issue is more than a performance budget concern because failed installation leaves users on the old worker or without the intended fresh offline shell.

Root cause

Deduplication scope is local to extractAppShellAssets(html). The seven routes are processed independently and concurrently; there is no installation-wide set of URLs and no Map<URL, Promise> to share in-flight work.

Recommended fix

Use installation-wide deduplication. Two reasonable designs are:

  1. Fetch/cache all localized HTML routes first, collect every extracted static URL into one global Set, then precache that set once.
  2. Keep route processing concurrent, but create a Map<string, Promise<void>> inside precacheAppShell() and have every route reuse the same promise for the same canonical URL.

Open the destination caches once per install where practical, and preserve explicit failure handling for genuinely missing required resources. Do not merely swallow asset failures to hide the duplicate-request symptom.

Regression coverage

Add a behavior-level Service Worker unit test with mocked fetch and caches:

  1. Return multiple localized HTML pages that reference overlapping static assets.
  2. Run precacheAppShell().
  3. Assert each page route is fetched as required.
  4. Assert each unique /_next/static/ URL is fetched exactly once.
  5. Assert each unique URL is written to the correct cache exactly once.
  6. Verify a required unique asset failure has the intended install behavior without multiplying requests.

The current tests in tests/unit/service-worker-cache-policy.test.ts and tests/guards/offline-fallback-page.test.ts check policy helpers and source structure, but they do not execute the install/precache graph or assert request cardinality.

Merge assessment: should be fixed in this PR because the duplicate graph is introduced by the new localized app-shell precaching implementation and directly affects PWA installation reliability.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[Low / Compatibility] SVG generators silently replace valid CSS colors with defaults

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 in both SVG Blob Generator and SVG Pattern Generator.

Affected code

  • src/features/tools/svg-blob-generator/utils.ts:8-18: normalizeSvgColor accepts only 3/4/6/8-digit hex tokens.
  • src/features/tools/svg-blob-generator/utils.ts:65-69: rejected fill/stroke values are silently replaced by #22d3ee / #0f172a.
  • src/features/tools/svg-pattern-generator/utils.ts:14-24: the same hex-only policy is duplicated.
  • src/features/tools/svg-pattern-generator/utils.ts:32-55 and :58-71: rejected foreground/background values are silently replaced by #22d3ee / #020617.
  • src/features/tools/svg-blob-generator/page.tsx:215-220 and src/features/tools/svg-pattern-generator/page.tsx:237-242: each color control includes an unrestricted text input. The UI keeps displaying the user's text even when the generated preview/export has substituted another value.

Reproduction / confirmed outputs

Enter any of these valid SVG/CSS colors into the text field:

red
rgb(1 2 3)
hsl(120 100% 50%)
transparent

Direct calls to the current builders produced the following behavior for every value above:

<!-- Blob output -->
<path ... fill="#22d3ee" stroke="#0f172a" ... />

<!-- Pattern output -->
<rect ... fill="#020617" />
<circle ... fill="#22d3ee" />

The input still displays red, rgb(...), hsl(...), or transparent, but the preview, copied SVG, downloaded SVG, and generated CSS/data URI use defaults instead.

Regression from main

Before this PR, the provided color string was serialized directly, so browsers could render standard CSS color syntaxes. The PR correctly attempts to prevent attribute/markup injection, and the new tests cover a payload such as:

"><script>alert(1)</script><rect fill="

However, replacing raw serialization with a hex-only whitelist also removes legitimate functionality. Existing tests prove that malicious markup is rejected but do not assert that ordinary non-hex colors remain supported.

Expected behavior

A color accepted by the tool should be represented consistently in the text field, color preview, rendered SVG, copied output, and download. If the product intentionally supports only hex, the text input must validate that format and clearly report an error rather than silently changing the output.

Actual behavior and impact

Users can copy or download an SVG whose colors differ from the values visibly present in the form. Named colors, modern rgb()/hsl() syntax, alpha colors, and transparent are all standard in SVG/CSS workflows, so pasted design-system tokens and common snippets regress.

This is a silent correctness issue: there is no invalid-state styling, message, or normalization back into the field to reveal that fallback values were used.

Recommended fix

Keep the injection protection, but validate and serialize colors with a real parser rather than concatenating raw input or limiting the grammar to hex. The repository already has a color parsing dependency that may be suitable for concrete colors. A safe implementation should:

  1. Parse only concrete supported color values.
  2. Serialize the parsed result into a canonical safe form (for example hex/hex-alpha or normalized rgb/rgba).
  3. Reject markup, quotes, URLs, var(...), and other context-dependent values unless intentionally supported and safely handled.
  4. Surface invalid input in the UI instead of silently substituting a default.

Using CSS.supports("color", value) alone and then inserting the original string into an XML attribute is not sufficient; the output must still be safely normalized/escaped.

If hex-only behavior is a deliberate product constraint, set an explicit input pattern/validation message, remove the misleading unrestricted text affordance, and ensure the visible field is normalized to the actual exported value.

Regression coverage

For both generators, add positive tests for:

red
rgb(1 2 3)
rgba(1, 2, 3, 0.5)
hsl(120 100% 50%)
transparent
#abc
#abcdef80

Keep the existing injection tests and add component coverage asserting that the form value, preview source, and exported SVG agree. Invalid strings should produce an explicit validation state, not an unannounced fallback.

Merge assessment: lower severity than the runtime/QR issues, but it should be resolved or explicitly documented before treating the project-wide audit remediation as complete.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[High] Env Parser silently drops parsed variables from every export format

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9. This is introduced by the new shared getExportableEnvVars filter.

Affected code

  • src/features/tools/env-parser/utils.ts:12-24: ENV_KEY_PATTERN applies a POSIX-shell-style identifier rule and getExportableEnvVars removes every non-matching parsed entry.
  • src/features/tools/env-parser/utils.ts:48-65: JSON, YAML, and Docker exports all use that same filtered list.
  • src/features/tools/env-parser/page.tsx:47-49: the page's parsed-variable list/count does not use the same rule; it counts every non-empty parsed key.
  • src/features/tools/env-parser/page.tsx:58, :121-131, and :173: export output is generated from the filtered list without any warning or invalid-row state.
  • src/features/pipeline/adapter-registry.ts:662-666: the Pipeline Builder Env Parser adapter calls the same exporter, so the loss is not limited to the standalone page.
  • tests/unit/env-parser-utils.test.ts:60-68: the new test explicitly codifies silent filtering for all three formats rather than testing an error or format-specific policy.

Reproduction / confirmed output

Input:

BAD-KEY=no
DATABASE.URL=postgres

Direct execution of the current parser/exporters produced:

{
  "parsed": [
    { "key": "BAD-KEY", "value": "no", "line": 1 },
    { "key": "DATABASE.URL", "value": "postgres", "line": 2 }
  ],
  "uiVariableCount": 2,
  "json": "{}",
  "yaml": "",
  "dockerArgs": ""
}

On the page, the header reports 2 variables and the table displays both rows, but selecting JSON produces {}, selecting YAML produces an empty output, and Copy reports success for the incomplete/empty result.

Regression from main

origin/main exported every non-comment, non-empty entry with a key. The PR introduced isValidEnvKey and then reused it across every output format. This was likely intended to prevent shell-command injection in Docker arguments, but JSON and YAML do not share POSIX variable-name constraints and can safely represent quoted string keys.

Dot and hyphen keys are accepted by multiple dotenv/config ecosystems even though they are not valid shell identifiers. More importantly, regardless of the chosen dialect, the current UI has already accepted, counted, and displayed these entries; silently deleting them at export time is internally inconsistent.

Expected behavior

Every parsed row must have an explicit, format-aware outcome:

  • preserve and safely quote the key when the selected format supports it; or
  • mark the row invalid/unsupported for that format and prevent a misleading successful export.

The displayed count/table and the exported result must not disagree without an actionable warning.

Actual behavior and impact

Generated configuration can be incomplete while looking successful. Missing values such as DATABASE.URL, vendor-specific keys, or hyphenated configuration names may only be noticed after deployment. In Pipeline Builder the same omission can propagate into later steps, producing a valid-looking but semantically incomplete JSON/YAML payload.

Because no error is returned, automation has no way to distinguish ?there were no variables? from ?all variables were discarded.? This is a silent data-loss issue.

Root cause

A validation rule appropriate to shell variable identifiers was placed in a shared pre-export filter. The filter has no format parameter, does not return diagnostics, and is separate from the page's definition of a parsed variable.

Recommended fix

Make validation format-aware and non-silent:

  • JSON: preserve arbitrary parsed string keys; JSON.stringify safely quotes them.
  • YAML: preserve keys using a real YAML serializer or safely quote both keys and values.
  • Docker/shell: apply the intended environment-name rule, but return structured diagnostics for rejected rows and show them in the UI/pipeline result. Confirm the exact Docker target grammar rather than assuming every output shares POSIX shell syntax.

Alternatively, if the product intentionally accepts only [A-Za-z_][A-Za-z0-9_]* at parse time, flag invalid lines immediately, exclude them from the ?variables? count/table, disable export until acknowledged/fixed, and provide line-numbered errors. Do not parse them as valid and later erase them.

Regression coverage

Add tests for BAD-KEY, DATABASE.URL, 1START, empty keys, and an actual injection-shaped key, separately for JSON, YAML, and Docker output. Also add:

  1. A component test asserting the variable count/table/export cannot disagree silently.
  2. A Copy test asserting success is not reported when selected-format validation failed.
  3. A Pipeline Builder adapter test confirming unsupported keys produce diagnostics rather than disappearing.
  4. Round-trip tests through JSON.parse / the YAML parser for preserved non-shell keys.

Merge assessment: blocking. The PR introduces silent configuration loss in both the standalone tool and pipeline automation.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[High] The default sample and ordinary Open Graph image URLs can never be previewed

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 with both direct utility execution and a fresh Chromium run against the PR preview.

Affected code

  • src/features/tools/open-graph-meta-generator/page.tsx:30-40: the default/sample image is https://example.com/og/release-2048.png.
  • src/features/tools/open-graph-meta-generator/page.tsx:59: preview eligibility is computed with normalizeSupportedOpenGraphPreviewUrl, not the general absolute-URL validator used for metadata.
  • src/features/tools/open-graph-meta-generator/page.tsx:132-143: Preview is disabled whenever that allowlist validator returns null; the disabled reason is the generic ?input required? message.
  • src/features/tools/open-graph-meta-generator/utils.ts:13 and :39-49: live preview permits only HTTPS URLs on instagram.com subdomains, i.ytimg.com, or vumbnail.com.
  • src/features/tools/open-graph-meta-generator/manifest.ts:12-20: the same narrow domains are published as the tool's complete external-request policy.
  • src/features/tools/open-graph-meta-generator/utils.ts:51-73: metadata generation still accepts any absolute HTTP(S) image URL, so the output policy and preview policy disagree.

Default-state browser reproduction

  1. Open /en/open-graph-meta-generator on the PR preview.
  2. Observe that Image URL is already populated with https://example.com/og/release-2048.png.
  3. Observe that Preview is disabled and its accessible/title reason says: Add input before running this action.
  4. Check ?I understand this action may request the disclosed external asset from my browser.?
  5. Preview remains disabled.
  6. Click Sample; it resets to the same unusable URL and state.

Fresh Chromium result:

{
  "image": "https://example.com/og/release-2048.png",
  "previewDisabledBeforeConfirmation": true,
  "previewDisabledAfterConfirmation": true,
  "disabledReason": "Add input before running this action."
}

Direct utility execution confirms the internal contradiction:

{
  "candidatePreviewImage": null,
  "metaContainsImage": true,
  "imageTag": "<meta property=\"og:image\" content=\"https://example.com/og/release-2048.png\" />"
}

Replacing the sample with a normal project/CDN image such as https://cdn.example.org/og.png has the same result. This is the dominant Open Graph use case: og:image usually belongs to the page owner's own site or CDN, not YouTube/Vimeo/Instagram.

The Instagram rule has another compatibility gap: hostname.endsWith(".instagram.com") does not cover the separate cdninstagram.com registrable domain used by common Instagram media URLs.

Expected behavior

The shipped default/sample must exercise the primary workflow successfully. A user should be able to enter the same valid image URL that is emitted into og:image, explicitly consent to the browser request, and preview it, subject to a clearly documented safety policy.

If the product intentionally supports only a fixed host set, the sample must use a real allowed image and the UI must explicitly report ?unsupported preview host,? including the supported hosts. It must not claim that a populated field is missing input.

Actual behavior and impact

The page advertises a live social-card preview, but its initial state, Sample action, and most real-world OG image URLs cannot invoke that feature. Metadata generation still includes those URLs, so users see an image tag in the output while being unable to validate it in the adjacent preview. The generic disabled reason sends them toward editing an already populated field without identifying the actual policy restriction.

Root cause

The PR correctly added explicit external-image consent, but coupled it to a platform-specific thumbnail allowlist copied from media tools. That policy does not match a general Open Graph generator, and the sample/default was not updated to satisfy the new rule. Eligibility failure is represented as ?no candidate,? losing the distinction between empty, invalid, insecure, credentialed, unsupported-host, and offline cases.

Recommended fix

First decide and document the intended preview threat model:

  • If arbitrary user-owned OG images are the intended workflow, support a carefully validated HTTPS URL after explicit consent, retain referrerPolicy="no-referrer", reject credentials/nonstandard ports and local/private targets as appropriate, and align CSP plus manifest disclosure with that behavior.
  • If a strict host allowlist is required, provide a real reachable sample on an allowed host, include all intentionally supported CDN hostnames, expose a structured ?unsupported host? reason, and change the product copy so it does not imply general OG-image preview support.

In either design, normalizeSupportedOpenGraphPreviewUrl should return structured validation results rather than collapsing all failures to null, allowing the action bar to show the correct reason.

Regression coverage

Add component/browser tests that:

  1. Load the default state or click Sample.
  2. Confirm consent.
  3. Complete one successful preview using the shipped sample.
  4. Exercise the intended policy for an ordinary HTTPS OG image.
  5. Assert unsupported hosts receive a specific unsupported-host message, not ?Add input.?
  6. Assert metadata output and preview eligibility do not silently apply contradictory URL policies.
  7. Cover intended Instagram CDN hostnames if Instagram remains part of the allowlist.

The current component test replaces the default with an allowed fictional YouTube URL before confirming, so it cannot detect the broken default/sample workflow.

Merge assessment: blocking for this tool because the PR disables the primary preview workflow in its own default state and for typical user inputs.

@baixiangcpp

Copy link
Copy Markdown
Owner Author

[Medium] Open Graph Preview reports success before the remote image has loaded

Status: Confirmed on PR head 143b21624589be36b527d9cd2c09e5d8f1329cc9 with an allowed-host URL whose response is not a valid image.

Affected code

  • src/features/tools/open-graph-meta-generator/page.tsx:82-109: handlePreview validates consent/offline state, calls setApprovedPreviewImage(...), and immediately calls notifyToolActionSuccess(...).
  • src/features/tools/open-graph-meta-generator/page.tsx:221-232: the <img> has no onLoad or onError handling and there is no loading/error state.
  • tests/component/external-request-media-tools.test.tsx:132-157: the test asserts only that an <img> with the requested src appears; it never fires or waits for an image load/error event.

Browser reproduction / evidence

  1. Enter an allowlisted URL that does not decode as an image, for example https://i.ytimg.com/robots.txt.
  2. Confirm the external request.
  3. Click Preview.

In fresh Chromium, 100 ms after the click the application had already mounted a success toast containing:

Preview completed.

At that same moment the image state was:

{
  "src": "https://i.ytimg.com/robots.txt",
  "complete": false,
  "naturalWidth": 0,
  "naturalHeight": 0
}

After the request completed/failed to decode, naturalWidth and naturalHeight remained 0, but no failure feedback replaced the success state. The preview area shows a broken image while the action is recorded as completed successfully.

The same behavior occurs for an allowlisted 404, corrupt image, blocked response, CSP failure, DNS/network interruption after the click, or a URL that returns HTML/text instead of image bytes.

Expected behavior

The action should enter a pending/loading state when the URL is assigned. Success should be announced only from the image's actual load event after it has non-zero intrinsic dimensions. The error event should produce failure feedback and a recoverable preview state.

Actual behavior and impact

The UI equates ?React accepted a new src string? with ?the external image preview completed.? Users receive a false success signal for unavailable or invalid assets and may publish metadata believing the image was verified. Assistive-technology users receive the same incorrect announcement, with no subsequent error status.

Root cause

setApprovedPreviewImage is asynchronous state scheduling, not network/image-decode completion. The current implementation has no state machine connecting the action invocation to the <img> lifecycle, so it cannot distinguish pending, loaded, and failed resources.

Recommended fix

Model the preview lifecycle explicitly, for example idle | pending | loaded | error plus the candidate/request ID:

  1. On Preview, set the approved URL and status to pending; do not emit success yet.
  2. On <img onLoad>, confirm the event belongs to the latest approved URL, verify naturalWidth > 0, set loaded, and emit success.
  3. On <img onError>, set error, clear or retain the failed URL with an error placeholder, and emit notifyToolActionFailure with an image-load-specific message.
  4. If the URL changes or consent is revoked, invalidate any prior load event so it cannot announce success for stale input.
  5. Expose pending/error state in the preview and action semantics; avoid allowing repeated success clicks while the same request is still pending.

Regression coverage

Extend the component test with controlled image events:

  • After clicking Preview, assert no success feedback exists before load.
  • fireEvent.load(img) and then assert success.
  • In a separate test, fireEvent.error(img) and assert failure feedback, no success, and the intended failed-preview UI.
  • Change the URL while the first image is pending and ensure a late first load event cannot mark the second URL successful.
  • Add a browser smoke case using a deterministic failed route/response if the test server can provide one.

Merge assessment: should be fixed in this PR because the new explicit-preview action currently makes a false correctness claim about external network work.

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