@produtype/core 0.85.0 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,42 @@ const ORIGIN_HANDLING = [/Access-Control-Allow-Origin/i, /ALLOWED_ORIGINS/, /all
19
19
  * the first was being caught.
20
20
  */
21
21
  const WILDCARD_ORIGIN = /(Access-Control-Allow-Origin["'\s:,]+\*)|(["']\*["'])/i;
22
+ /**
23
+ * A line that names an origin somebody chose.
24
+ *
25
+ * The test is that the value is written down: a quoted string that is not `*`, or a
26
+ * list of them. Everything else — a variable, a concatenation, a method call — is a
27
+ * value the reader of this line cannot see, and a security check does not get to
28
+ * assume it is an allowlist.
29
+ */
30
+ function allowsAChosenOrigin(line, file) {
31
+ /**
32
+ * The line itself asks whether the origin belongs.
33
+ *
34
+ * `!ALLOWED_ORIGINS.includes(origin)` is the allowlist being enforced, and it is
35
+ * the strongest evidence there is — stronger than the declaration it checks
36
+ * against, which may live in an environment variable.
37
+ */
38
+ if (MEMBERSHIP_TEST.test(line))
39
+ return true;
40
+ const assigned = /(?:Access-Control-Allow-Origin|ALLOWED_ORIGINS|allowedOrigins)[^=:,]*[=:,]\s*(.+)$/i.exec(line);
41
+ if (!assigned)
42
+ return false;
43
+ // An origin written down: it has a scheme or a dotted host, which `","` does not.
44
+ if (/["'`](?:https?:\/\/|\*\.)[^"'`]*["'`]|["'`][^"'`]*\.[a-z]{2,}[^"'`]*["'`]/i.test(assigned[1]))
45
+ return true;
46
+ /**
47
+ * Or a value this line cannot see, checked somewhere else in the same file.
48
+ *
49
+ * A list kept in an environment variable is still an allowlist, so the test cannot
50
+ * be "is the value a literal". What separates it from reflection is that somebody
51
+ * asks whether the origin belongs before answering yes — and shopizer never does:
52
+ * `origin = request.getHeader("origin")` goes straight into the header.
53
+ */
54
+ return MEMBERSHIP_TEST.test(file);
55
+ }
56
+ /** `includes`, `contains`, `indexOf`, `has` — asking whether a value belongs. */
57
+ const MEMBERSHIP_TEST = /\.(includes|contains|indexOf|has)\s*\(/i;
22
58
  /**
23
59
  * Flask's answer, which was invisible.
24
60
  *
@@ -287,8 +323,25 @@ async function detectSecurity(ctx) {
287
323
  for (const m of (0, textSearch_1.matchLines)(text, ORIGIN_HANDLING, file)) {
288
324
  if (WILDCARD_ORIGIN.test(m.snippet))
289
325
  corsLoose.push(m);
290
- else
326
+ else if (allowsAChosenOrigin(m.snippet, text))
291
327
  corsStrict.push(m);
328
+ /**
329
+ * Anything else sets the header to a value this cannot see.
330
+ *
331
+ * shopizer writes `origin = request.getHeader("origin")` and then
332
+ * `setHeader("Access-Control-Allow-Origin", origin)` — reflecting whatever the
333
+ * caller asked for, which allows every origin there is. The report called it
334
+ * "configured with explicit origins" and passed it: a clean verdict on the one
335
+ * shape this check exists to catch.
336
+ *
337
+ * An allowlist is a fixed set, so it is written down. A variable, a
338
+ * concatenation or a call cannot be shown to be one — it may be a value from
339
+ * configuration, and it may be the request's own header, and nothing in the line
340
+ * says which. `partial` is what that deserves: something is configured and
341
+ * nothing here shows it restricted.
342
+ */
343
+ else
344
+ corsLoose.push(m);
292
345
  }
293
346
  // Only where the middleware is actually imported. A sentence in this tool's own
294
347
  // remediation catalogue — "Replace cors() defaults with an explicit allowlist." —
@@ -391,6 +444,26 @@ async function detectSecurity(ctx) {
391
444
  corsStrict.push(hit);
392
445
  break;
393
446
  }
447
+ /**
448
+ * ASP.NET Core's, which is a call to the framework's own builder.
449
+ *
450
+ * `AddCors()` registers the service and `UseCors()` puts it in the pipeline; both
451
+ * names belong to the framework. Jellyfin calls each of them and was told at `high`
452
+ * that it has no cross-origin configuration.
453
+ *
454
+ * Which policy it is comes from the builder: `AllowAnyOrigin()` is the wide-open
455
+ * one and `WithOrigins(...)` is a list somebody chose. Jellyfin has both — the
456
+ * first when no hosts are configured — and where both are shipped the open branch
457
+ * is the one worth reporting, because it is reachable.
458
+ */
459
+ const aspNetCors = await (0, textSearch_1.searchInFiles)(ctx.root, source.filter((f) => /\.cs$/i.test(f)), [/\.\s*AddCors\s*\(/, /\.\s*UseCors\s*\(/, /\bUseCors\s*\(/, /\bAddCors\s*\(/], 5);
460
+ if (aspNetCors.length > 0) {
461
+ const anyOrigin = await (0, textSearch_1.searchInFiles)(ctx.root, source.filter((f) => /\.cs$/i.test(f)), [/AllowAnyOrigin\s*\(/], 2);
462
+ const chosen = await (0, textSearch_1.searchInFiles)(ctx.root, source.filter((f) => /\.cs$/i.test(f)), [/WithOrigins\s*\(/, /SetIsOriginAllowed\s*\(/], 2);
463
+ const target = anyOrigin.length > 0 || chosen.length === 0 ? corsLoose : corsStrict;
464
+ const cited = anyOrigin[0] ?? chosen[0] ?? aspNetCors[0];
465
+ target.push({ file: cited.file, line: cited.line, snippet: cited.snippet.trim().slice(0, 200) });
466
+ }
394
467
  const django = await (0, djangoSettings_1.findDjangoSettings)(ctx);
395
468
  if (django) {
396
469
  const middlewareLine = django.text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.85.0",
3
+ "version": "0.87.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {