Code Audit Report — 2026-04-23 #1
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Code Audit Report — 2026-04-23
Automated audit of
webapp-scaffoldcovering 23 audit categories.webapp-scaffoldis a small (~750 LOC, 8 source files) shared npm library (@uschuster/webapp-scaffold) providing frontend build glue for derived projects: Vite config factories, an Orvalfetchmutator (createCoreFetch), a tiny i18n store, and three codegen/build CLIs (fetch-openapi.sh,postprocess-openapi.py,inject-hashed-filenames.py). It is not an application: no Oat++ backend, no SQLite, no authentication, no controllers, no user input handling, no file uploads, no server-side logging, no tests. Most of the 23 prompts target application concerns that simply do not exist here; findings reflect what is present.Executive Summary
The scaffold is small, focused, well-commented, and has no obvious security issues in its own code. The main gaps are library hygiene: no tests,
preparescript swallowstscerrors, Python scripts use unsafe shell-style string substitution in one place, and there is no input validation / path-traversal defense in the build CLIs (acceptable as developer-only tools, but worth noting).Security Findings
1. Initial Security Analysis
bin/fetch-openapi.shembeds$OUTpath directly inside a heredoc passed topython3 -c. IfOPENAPI_OUTwere attacker-controlled (it is not in practice — it's a dev env var), it would allow Python code injection via quote-breaking in the filename. Fix: pass$OUTviasys.argvrather than f-string substitution in the script heredoc.bin/fetch-openapi.sh:32-37curl -fsS "$URL"with no pinned CA or fingerprint. URL defaults tohttp://127.0.0.1, so not an issue by default, but documented support forOPENAPI_URL=https://…gives no transport-security hardening. Fine for dev, note in README.vite,@vitejs/plugin-react,react), devDeps (typescript,@types/node,@types/react).package-lock.jsonis 60 KB — routine.2. Session & Cookie Security
src/core-fetch.tsalways setscredentials: 'include', which is correct for derived SPAs that talk to a same-origin oatpp backend. No cookie parsing happens here.3. Authentication Flow Review
core-fetch.tsmerely forwards credentials and exposes anon401hook so consumers can redirect to a login page. The hook contract is fine:on401is a side-effect callback; the error is still thrown unless the non-OK branch is taken — which it will be for 401 sincer.okis false and no special swallow exists. Consumers relying onon401should NOT assume the error is swallowed.4. Input Validation (SQLi / XSS / injection)
bin/inject-hashed-filenames.pyperforms a naïvehtml.replace(old_src, new_src). Ifold_srcever occurred as a substring outside a<script src="…">attribute in an HTML file (e.g. inside a comment or another tag), it would be rewritten unexpectedly. Safer: parse the HTML or match on the full tag. Low exploitability since it operates on project-controlled static files, but it's a latent bug.bin/inject-hashed-filenames.py:50-52bin/postprocess-openapi.pytrustsopenapi.jsonstructure.paths→methodsiteration assumes dicts; malformed input willAttributeErrorwith an unhelpful message. Acceptable for a dev script but the bundledisinstance(op, dict)guard only coversop, notmethods.bin/postprocess-openapi.py:39-44createCoreFetchdoes not sanitiseurlbefore concatenating withbaseUrl. Relative-URL or absolute-URL bypass of the baseUrl prefix is trivial (anurlstarting withhttp://evilwill be sent toevilsince${base}${url}just concatenates). This matchesfetchsemantics and Orval-generated callsites always pass server-emitted paths, but document it.src/core-fetch.ts:1365. Authorization Implementation
6. Database Security
7. Secrets Management
bin/fetch-openapi.shreadsAPP_API_KEYfrom env and passes it as a Bearer token. Key is not logged (onlyURL → OUTis echoed). Acceptable..envfiles.package.jsonhas no embedded tokens..gitignoreis minimal (only 36 bytes — spot-checked; git log shows no history of leaked credentials).~/.npmrcregistry URL is documented in README as unauthenticated HTTP (http://127.0.0.1:3000/api/packages/uwe.admin/npm/). Expected (internal Forgejo), but_authTokenhandling is left to the consumer — README could remind devs the token lives in~/.npmrcand must never be committed.8. Business Logic Vulnerabilities
9. API & Infrastructure Security
createCoreFetchsetsX-Requested-With: XMLHttpRequestso the derived oatppCsrfInterceptoraccepts the request. Fine.'wrapped'while the README and fewo convention is'body'. Mismatch between docs and default could cause a consumer to silently get{data, status, headers}where they expected the body. Consider aligning the default, or calling the mismatch out more loudly in README.src/core-fetch.ts:104vsREADME.md:46-6210. Logging & Monitoring
createCoreFetchswallows failed fetches silently ifonNetworkFailurereturns a value. Consumers could unknowingly mask systemic outages. Acceptable because the hook is opt-in, but doc it.11. File Handling & Uploads
inject-hashed-filenames.pydoesos.path.join(build_dir, e["manifest"])— a config with"manifest": "../../etc/passwd"would read an arbitrary file. Dev-only tool so the threat is trust-in-your-own-repo level, not a real vulnerability.bin/inject-hashed-filenames.py:66-7112. Comprehensive Security Report (synthesis of 01–11)
The scaffold has no critical or high security findings. All "applicable" findings are Medium or Low and concentrated in the three build CLIs under
bin/. The TypeScript sources (core-fetch,i18n,vite-config) are defensive and well-factored. Top 3 actionable items:fetch-openapi.sh:32-37(pass$OUTvia argv, not f-string).inject-hashed-filenames.pytag-aware rather than substring-based.responseShape: 'wrapped'vs the README's'body'example, or flip the default.Code Quality Findings
13. Architecture & Design
vite-config), HTTP mutator (core-fetch), i18n (i18n+ React bindingi18n-react), and codegen CLIs (bin/).package.jsonexportsmap is precise — each entry point has its own types/ESM bundle. No accidental re-exports.dist/is listed infilesbut thepreparescript doestsc || true. Publishing with a broken type build would silently ship no dist. Replace with stricttscand move error tolerance (if desired) to a CI step.package.json:3514. Code Duplication
defineAdminConfiganddefineGuestConfigshare ~70 % structure; extraction intodefineViteConfig(common)would save ~15 lines but costs clarity. Current duplication is acceptable at this size.15. Error Handling
src/core-fetch.tshas consistent error branches: network failure → hook → rethrow; 401 → hook → continue; 409 → hook → swallow-or-throw withdecorate409; other !ok →formatErrorhook → throw. Flow is clear and documented.core-fetch.ts204 + non-JSON content-type path returnsawait r.text()as the parsed body. A non-application/json200 OKwill silently return the raw text where the caller's generated type expects an object. This matches no Orval response type. Prefer erroring on unexpected content-type, or at least documenting.src/core-fetch.ts:175-178readBody()catches both.json()and.text()failures and returns['', null]. Reasonable for test-mock resilience; ensure a comment captures the tradeoff (it already does).16. Exception Flow Analysis
core-fetch.tsthrows. Other modules are pure data.createI18nnever throws; missing keys log viaonMissand return the raw key — correct for UX.!r.ok, which throws). Callers depending on the hook to redirect-and-never-return will still see a thrown rejection. Document explicitly.17. Code Quality Metrics
core-fetch.tsat 214 lines. Largest function:coreFetchclosure at ~65 lines. Cyclomatic complexity is low everywhere; no file warrants splitting.18. Design Patterns
createCoreFetch,createI18n,defineAdminConfig) used appropriately — returns closures with captured config. No globals.createI18n(listeners+subscribe) integrates correctly with React'suseSyncExternalStore. Good.19. SOLID Principles
createCoreFetchis open via hook options; closed for modification.fetchImplinjection increateCoreFetchallows test-time substitution — canonical DIP.20. Testing Implementation
test/,tests/,spec/,__tests__/, no vitest/jest config, nonpm testscript. ThefetchImplinjection point exists specifically for testing but is never exercised inside the scaffold itself. Given the library is consumed by multiple projects, unit tests forcreateCoreFetch(response shapes, 401/409 hooks, network-failure hook, enqueue short-circuit) andcreateI18n(resolver chain, tone fallback, miss logging, subscribe/notify) would be high-leverage.21. Readability & Naming
CoreFetchOptions,I18nOptions). Inline comments explain the why (e.g. whyfetchImplresolves lazily; why.json()is tried before.clone()).MUTATINGconstant is clear.chain()ini18n.tsis a touch cryptic — considerresolveLookupOrder(). Minor.22. Resilience & Fault Tolerance
onNetworkFailurehook is the single resilience seam and is well-designed (synthetic 0-status wrap mirrors real responses).createI18nis pure and deterministic — no fault surface.23. Orval Codegen Duplication
Prioritized Remediation Plan
$OUTto the embedded Python viasys.argv, not f-string substitution, to close the injection-shaped pattern —bin/fetch-openapi.sh:32-37html.replace(old_src, new_src)with tag-aware rewrite (regex on<script src="…">or a tiny HTML parser) —bin/inject-hashed-filenames.py:50-52createCoreFetch(each hook, each response shape, 401/409/network paths) andcreateI18n(resolver chain, miss cache, subscribe/notify). Wirenpm testintoprepareresponseShape: either flip to'body'to match the README example or update README and every derived project's call site to passresponseShape: 'body'explicitly —src/core-fetch.ts:104|| truefrom thepreparescript so a brokentscfails the publish —package.json:35on401does NOT swallow the error — callers must still.catch()or rely on the navigation to unmount —src/core-fetch.ts:151+ READMEcore-fetchrather than silently returning text —src/core-fetch.ts:175-178postprocess-openapi.pyagainst malformedpathsvalues (isinstance(methods, dict)guard, helpful error) —bin/postprocess-openapi.py:39-44inject-hashed-filenames.pytobuild_dirto defend against../traversal even from a trusted config —bin/inject-hashed-filenames.py:66-71~/.npmrcauth-token hygiene and call out that registry URL is plain HTTP (localhost-only) —README.md:25-29Agent Evaluation
This issue is a code audit report (filed by the /code-audit skill), not a feature request. No evaluation action needed from the poll loop — each line in the Prioritized Remediation Plan is itself a checkbox that can be split into discrete fix issues by the owner on demand.
Feasibility: N/A — tracking issue.
Impact: Informational; owner triages findings.
Effort: N/A (individual fixes range small-to-medium).
Recommendation: Leave open as an index; close once all prioritized checkboxes are addressed or split into child issues.
Labeling
evaluated+effort:mediumso the loop skips it on future passes.Evaluated #1 — code audit tracking issue, labeled evaluated+effort:medium to exclude from future poll cycles.
Poll cycle: nothing actionable — audit tracker remains the only open issue.
Split into child issues, please.
Re-evaluation
Done. Filed one child issue per MEDIUM finding so they can be evaluated, prioritised, and accepted independently:
$OUTviasys.argv, not f-string in heredoccreateCoreFetch+createI18n; wire intopreparecreateCoreFetchdefaultresponseShapeand align README + derived projectsThe LOW findings I left in this tracker rather than spinning up six more issues — they're docstring/hardening tweaks the next pass over each file can clean up alongside the MEDIUM in that same area. If you'd like them split too, say the word.
This issue stays open as the audit index — close once #2–#5 (and the LOW items inline) are resolved.
Re-evaluated #1 — split MEDIUM findings into child issues #2/#3/#4/#5 per owner request; LOW items remain inline in tracker.
Tracker status
All child issues now closed; closing this tracker.
b1a13b8, v0.3.6)5ee6894)Any further audit follow-ups can be filed as fresh issues.