@fuzdev/fuz_app 0.144.0 → 0.145.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.
@@ -5,37 +5,112 @@
5
5
  * nginx location being `internal;` — only the authz'd handler's redirect (an
6
6
  * internal subrequest) may reach it. A *public* facts location would serve any
7
7
  * fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
8
- * cell-visibility check. `XAccelConfig` makes that assertion structural: a
9
- * consumer can only obtain the redirect prefix by building an `XAccelConfig`,
10
- * which runs this check and throws **loudly** on a missing or non-`internal;`
11
- * location — it cannot silently fall closed.
8
+ * cell-visibility check. A location with its own `add_header` also stops the
9
+ * server-level headers from applying, so the facts location must carry the fact
10
+ * hardening headers itself (`fact_security_header_directives` in
11
+ * `server/fact_security_headers.ts`).
12
12
  *
13
- * This is a best-effort string check (brace-matched `location` blocks), the TS
14
- * twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
15
- * it catches the security-critical omission. This is the only nginx property
16
- * fuz_app validates; there is no general config validator.
13
+ * `XAccelConfig` makes these assertions structural: a consumer can only obtain
14
+ * the redirect prefix by building an `XAccelConfig`, which runs
15
+ * `validate_facts_internal_location` and throws **loudly** on any violation — it
16
+ * cannot silently fall closed.
17
+ *
18
+ * ## Rules
19
+ *
20
+ * The config is tokenized as nginx reads it (`ngx_conf_read_token`): whitespace
21
+ * is exactly space, tab, CR and LF; an unquoted word ends only at those or at
22
+ * `;` / `{` (a `}` inside a word is part of it, and so is a `{` straight after
23
+ * `$`); `#` starts a comment only where a token would start; quoted strings are
24
+ * one word, with nginx's backslash escapes. It is then parsed into nested
25
+ * blocks, and every `location` at any depth, in every `server` block, is
26
+ * classified against the prefix (trailing slashes ignored). A location *serves
27
+ * the prefix* when it is a prefix-type location (no modifier, or `^~`) whose
28
+ * path is the prefix or below it. Each one must:
29
+ *
30
+ * - use `^~` — a plain prefix location is refused;
31
+ * - be a direct child of a `server` block — one nested in another location is
32
+ * refused;
33
+ * - carry, as top-level directives of its own body, an exact `internal;` and
34
+ * each `fact_security_header_directives` line (token-equal: whitespace and
35
+ * quoting are free, the words are not);
36
+ * - hold only simple directives — any nested block (`location`, `if`,
37
+ * `limit_except`, …) is refused, and so is `include`, since included files
38
+ * are not followed.
39
+ *
40
+ * The first two rules are what keep regex locations out: nginx never evaluates
41
+ * regex locations for a path whose longest prefix match is a `^~` location at
42
+ * server level, and because every prefix location at or below the prefix must
43
+ * be such a location, the longest match for any redirected path always is. (A
44
+ * longer plain location below the prefix can't exist — the `^~` rule refuses
45
+ * it.)
46
+ *
47
+ * The prefix may appear only in its own location lines and their bodies:
48
+ * `internal;` admits every internal redirect and subrequest, so any other
49
+ * directive with a word naming the prefix (trailing slash trimmed, so
50
+ * `/_facts$uri` counts) is refused, in any block at any depth — a `rewrite`,
51
+ * `try_files`, `error_page`, `index`, `add_before_body`, or a `set` / `map`
52
+ * value that a redirect later reads. The only exemptions are each `location`'s
53
+ * own modifier and path words and the bodies of the locations serving the
54
+ * prefix.
55
+ *
56
+ * Also refused: an `=` location on the prefix or below it (it can't serve the
57
+ * nested paths the redirect emits); a regex location (`~` / `~*`) whose pattern
58
+ * spells out the prefix (after unescaping `\/`, ASCII case-folded for `~*`) —
59
+ * dead under the rules above, refused for the clearer message; and a config
60
+ * with no location serving the prefix. `include` at server level is allowed,
61
+ * but its file is not read — keep facts-related locations out of included
62
+ * files.
63
+ *
64
+ * The threat model is operator mistakes, not an adversarial config author: the
65
+ * prefix check is textual, so a value assembled from pieces (`/_fac$1`) evades
66
+ * it. Beyond the config text:
67
+ *
68
+ * - The validator checks URL paths only — another `root` or `alias` that maps
69
+ * onto the facts directory would serve its files publicly, so keep the facts
70
+ * directory outside every other location's root.
71
+ * - nginx honours `X-Accel-Redirect` from any proxied upstream (`proxy_pass`,
72
+ * `fastcgi_pass`, `uwsgi_pass`, `scgi_pass`, `grpc_pass`), so every upstream in
73
+ * the server is trusted to point into the prefix; an untrusted one needs
74
+ * `proxy_ignore_headers X-Accel-Redirect;` (or the `fastcgi_` / `uwsgi_` /
75
+ * `scgi_` equivalent).
76
+ * - Module code can redirect internally too (ssi `include virtual`, njs
77
+ * `internalRedirect`, perl, echo), so run trusted modules and content only.
78
+ *
79
+ * The Rust twin is `fuz_fact_serving`'s `nginx.rs`, held to the same rules and
80
+ * the same fixture corpus.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const x_accel = create_x_accel_config(
85
+ * '/_facts/',
86
+ * `server {
87
+ * location ^~ /_facts/ {
88
+ * internal;
89
+ * alias /srv/app/facts/;
90
+ * ${fact_security_header_directives().join('\n\t\t')}
91
+ * }
92
+ * }`
93
+ * );
94
+ * ```
17
95
  *
18
96
  * @module
19
97
  */
20
98
  import { z } from 'zod';
21
99
  /** Result of the facts-location check. */
22
100
  export interface NginxFactsValidation {
23
- /** `true` when the facts location exists and is `internal;`. */
101
+ /** `true` when every rule passes. */
24
102
  ok: boolean;
25
- /** Fatal issues — a missing or non-`internal` facts location. */
103
+ /** Fatal issues, each naming the offending location. */
26
104
  errors: Array<string>;
27
105
  }
28
106
  /**
29
- * Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
30
- *
31
- * `facts_location` is the path the X-Accel redirect prefix points at (e.g.
32
- * `/_facts/`). Returns a fatal error when no matching `location` block exists,
33
- * or when the matching block is not marked `internal;` — either is a public
34
- * facts location that bypasses cell visibility.
107
+ * Assert the nginx config serves the X-Accel facts prefix only from `internal;`
108
+ * locations carrying the fact hardening headers. See the module docs for the
109
+ * rules; every violation is collected, so one call reports them all.
35
110
  *
36
- * @param config - the nginx config template string to check
37
- * @param facts_location - the facts location path the redirect prefix points at
38
- * @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
111
+ * @param config - the nginx config string to check
112
+ * @param facts_location - the facts location path the redirect prefix points at (e.g. `/_facts/`)
113
+ * @returns `{ok, errors}` — `ok` is `true` only when every rule passes
39
114
  */
40
115
  export declare const validate_facts_internal_location: (config: string, facts_location: string) => NginxFactsValidation;
41
116
  /**
@@ -44,10 +119,11 @@ export declare const validate_facts_internal_location: (config: string, facts_lo
44
119
  *
45
120
  * The redirect prefix can be obtained **only** by passing the nginx config
46
121
  * through `validate_facts_internal_location` (via `create_x_accel_config`), so
47
- * X-Accel serving is impossible to enable without proving the facts location is
48
- * `internal;` at boot — a public facts location would bypass every
49
- * cell-visibility check. A Zod-branded type: the brand can't be forged without
50
- * an explicit cast, so the factory is the only ordinary construction path.
122
+ * X-Accel serving is impossible to enable without proving at boot that the
123
+ * facts location is `internal;` and carries the fact hardening headers — a
124
+ * public facts location would bypass every cell-visibility check. A
125
+ * Zod-branded type: the brand can't be forged without an explicit cast, so the
126
+ * factory is the only ordinary construction path.
51
127
  */
52
128
  export declare const XAccelConfig: z.core.$ZodBranded<z.ZodObject<{
53
129
  redirect_prefix: z.ZodString;
@@ -63,18 +139,18 @@ export declare class XAccelConfigError extends Error {
63
139
  constructor(errors: Array<string>);
64
140
  }
65
141
  /**
66
- * Build a validated `XAccelConfig`, asserting the nginx `location` serving
67
- * `redirect_prefix` is `internal;`.
142
+ * Build a validated `XAccelConfig`, asserting the nginx config serves
143
+ * `redirect_prefix` only from safe locations (the module-level rules).
68
144
  *
69
145
  * The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
70
- * into a location proven `internal;` at boot.
146
+ * into a location proven `internal;` and header-carrying at boot.
71
147
  *
72
148
  * @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
73
- * @param nginx_config - the nginx config template string to validate against
149
+ * @param nginx_config - the nginx config string to validate against
74
150
  * @returns the validated `XAccelConfig` carrying `redirect_prefix`
75
- * @throws `XAccelConfigError` when `nginx_config` has no matching `location` for
76
- * `redirect_prefix`, or the matching block is not marked `internal;` — either
77
- * is a public facts location that bypasses every cell-visibility check.
151
+ * @throws `XAccelConfigError` carrying every violation
152
+ * `validate_facts_internal_location` finds — a missing, public, header-less,
153
+ * exact-match, or regex-shadowed facts location.
78
154
  */
79
155
  export declare const create_x_accel_config: (redirect_prefix: string, nginx_config: string) => XAccelConfig;
80
156
  //# sourceMappingURL=x_accel.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;;0CAKF,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
1
+ {"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,qCAAqC;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,wDAAwD;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAYF,CAAC;AAsHF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY;;0CAKF,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}