@boldsec/next 0.1.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.
- package/README.md +78 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +312 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# @boldsec/next
|
|
2
|
+
|
|
3
|
+
Live BOLA/IDOR monitoring for **Next.js (App Router)**. Wrap an API route; BoLD watches it and
|
|
4
|
+
raises an alarm the moment one user reaches another user's object — in real time, from your app's
|
|
5
|
+
actual traffic.
|
|
6
|
+
|
|
7
|
+
- **Metadata only.** BoLD receives who made the request (an opaque, non-reversible handle — never
|
|
8
|
+
the token), the endpoint shape, the object id, the status, and the object's declared owner if the
|
|
9
|
+
response exposed one. The response body is read once to extract that single owner field and then
|
|
10
|
+
discarded. No body, no headers, no credentials ever leave your app.
|
|
11
|
+
- **Fail-safe.** Shipping the metadata is fire-and-forget. If BoLD is slow or down, your response is
|
|
12
|
+
returned untouched and on time. BoLD can never break or slow the app it watches.
|
|
13
|
+
- **Never alters your response.** Your handler runs exactly as before; its response is returned
|
|
14
|
+
byte-for-byte.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @boldsec/next
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configure (once)
|
|
23
|
+
|
|
24
|
+
Add your keys (from the BoLD **Live** page, shown once when you connect the app):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# .env.local
|
|
28
|
+
BOLD_INGEST_URL=https://<your-bold>/api/live/ingest
|
|
29
|
+
BOLD_INGEST_KEY=blk_...
|
|
30
|
+
# optional — owner-field names to look for in responses (default covers common ones)
|
|
31
|
+
BOLD_OWNER_FIELDS=ownerId,owner_id,user_id
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
Wrap the route handlers you want watched:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// app/api/invoices/[id]/route.ts
|
|
40
|
+
import { withBold } from "@boldsec/next";
|
|
41
|
+
|
|
42
|
+
export const GET = withBold(
|
|
43
|
+
async (req, ctx) => {
|
|
44
|
+
const invoice = await getInvoice(ctx.params.id);
|
|
45
|
+
return Response.json(invoice); // { id, ownerId, ... }
|
|
46
|
+
},
|
|
47
|
+
// STRONGLY RECOMMENDED — tell BoLD who the caller is, in the SAME namespace as your owner field:
|
|
48
|
+
{ resolveCallerId: (req) => getUserIdFromSession(req) }, // e.g. "usr_101"
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Works for `GET` / `PUT` / `PATCH` / `POST` / `DELETE` handlers. Redeploy, and your app's real
|
|
53
|
+
traffic is watched automatically — catches appear in your BoLD **Findings** with a live timestamp.
|
|
54
|
+
|
|
55
|
+
### App shapes it reads
|
|
56
|
+
|
|
57
|
+
- **REST object routes** — string ids (`/api/invoices/inv_8101`), numeric ids (`/api/orders/4021`),
|
|
58
|
+
UUIDs.
|
|
59
|
+
- **Nested / oddly-named owner fields** — the owner is found wherever it nests (bounded depth) as
|
|
60
|
+
long as the field name is in your owner-field list (e.g. add `holderId` for `data.account.holderId`).
|
|
61
|
+
- **GraphQL** — wrap your `app/api/graphql/route.ts` the same way. BoLD parses the operation and its
|
|
62
|
+
`id` (from `variables` or an inline literal) and reads the owner from `data.<field>`. Single-object
|
|
63
|
+
queries and mutations are watched; batched/list queries, or an id BoLD can't resolve, are skipped
|
|
64
|
+
(it never guesses — a shape it can't read is held for review, never reported clean).
|
|
65
|
+
|
|
66
|
+
### Why `resolveCallerId` matters
|
|
67
|
+
|
|
68
|
+
Your app already authenticated the caller, so it knows their id (e.g. `usr_101`). Passing it lets
|
|
69
|
+
BoLD compare the **caller** against the **object's owner** directly: if they match, it's the owner's
|
|
70
|
+
own access and BoLD stays silent — no false alarm. Without it, BoLD falls back to a non-reversible
|
|
71
|
+
hash of the auth header, which can't be matched against your owner ids, so a user reading **their
|
|
72
|
+
own** object may be flagged. Always provide `resolveCallerId` in production.
|
|
73
|
+
|
|
74
|
+
## How it confirms a violation
|
|
75
|
+
|
|
76
|
+
BoLD confirms a BOLA when a request returns an object whose **owner field** identifies a *different*
|
|
77
|
+
user than the caller. If a response doesn't expose an owner field, BoLD reports **needs review**
|
|
78
|
+
rather than guessing — it never raises a false alarm.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @boldsec/next: the Next.js (App Router) route wrapper for BoLD live monitoring (Stage 3 / S3.3).
|
|
3
|
+
*
|
|
4
|
+
* Why a route wrapper and not `proxy.ts`/middleware: Next.js proxy runs BEFORE the response and
|
|
5
|
+
* cannot read the response body — but confirming a BOLA needs the owner field FROM the response
|
|
6
|
+
* (e.g. `ownerId`). The route handler is the only place that sees both the request and the response,
|
|
7
|
+
* so that is where BoLD watches. The user wraps their existing handler; it runs unchanged.
|
|
8
|
+
*
|
|
9
|
+
* What it sends to BoLD: METADATA ONLY — who made the request (an opaque, non-reversible identity
|
|
10
|
+
* handle, never the token/cookie), the endpoint shape, the object id, the status, and the object's
|
|
11
|
+
* declared owner if the response JSON exposed one. The response body is read once to extract that
|
|
12
|
+
* single owner field and then DISCARDED. No body, no headers, no credentials ever leave the app.
|
|
13
|
+
*
|
|
14
|
+
* Failure mode: FAIL-SAFE. Shipping the metadata is fire-and-forget; if BoLD is slow or down, the
|
|
15
|
+
* user's response is returned untouched and on time. BoLD can never break or slow the app it watches.
|
|
16
|
+
*
|
|
17
|
+
* Usage (a few lines, no terminal):
|
|
18
|
+
*
|
|
19
|
+
* // app/api/invoices/[id]/route.ts
|
|
20
|
+
* import { withBold } from "@boldsec/next";
|
|
21
|
+
*
|
|
22
|
+
* export const GET = withBold(
|
|
23
|
+
* async (req, ctx) => {
|
|
24
|
+
* const invoice = await getInvoice(ctx.params.id);
|
|
25
|
+
* return Response.json(invoice); // { id, ownerId, ... }
|
|
26
|
+
* },
|
|
27
|
+
* { resolveCallerId: (req) => getUserIdFromSession(req) }, // RECOMMENDED — see BoldConfig
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* Config via env (set once): BOLD_INGEST_URL, BOLD_INGEST_KEY, optional BOLD_OWNER_FIELDS.
|
|
31
|
+
*/
|
|
32
|
+
export interface BoldConfig {
|
|
33
|
+
/** Where to send events — your BoLD ingest endpoint. Defaults to env BOLD_INGEST_URL. */
|
|
34
|
+
ingestUrl?: string;
|
|
35
|
+
/** The monitor's ingest key (shown once when you connect the app). Defaults to env BOLD_INGEST_KEY. */
|
|
36
|
+
ingestKey?: string;
|
|
37
|
+
/** Owner-field names to look for in the response JSON. Defaults to env BOLD_OWNER_FIELDS or a
|
|
38
|
+
* common set. The FIRST one present (top level or one level of nesting) is used. */
|
|
39
|
+
ownerFields?: string[];
|
|
40
|
+
/** The authenticated caller's RESOLVED id (e.g. `usr_101`) — same namespace as the response's
|
|
41
|
+
* owner field. STRONGLY RECOMMENDED: your app already authenticated the user, so it knows this.
|
|
42
|
+
* With it, BoLD sees "caller IS the owner" and never false-CONFIRMs an owner's own access. Pass
|
|
43
|
+
* a `(request) => id` resolver. Without it, identity falls back to a non-reversible hash of the
|
|
44
|
+
* auth header — which cannot match the owner namespace, so an owner reading their own object can
|
|
45
|
+
* be wrongly flagged. */
|
|
46
|
+
resolveCallerId?: (request: Request) => string | null | Promise<string | null>;
|
|
47
|
+
}
|
|
48
|
+
/** The shape of a Next.js App Router route handler: (request, context) => Response. We keep the
|
|
49
|
+
* context generic so any `{ params }` shape passes through untouched. */
|
|
50
|
+
export type RouteHandler<C> = (request: Request, context: C) => Response | Promise<Response>;
|
|
51
|
+
/** Split a path into an endpoint template (".../{id}") + the object id, or null if no id segment. */
|
|
52
|
+
export declare function endpointAndObject(pathname: string): {
|
|
53
|
+
endpoint: string;
|
|
54
|
+
objectId: string | null;
|
|
55
|
+
};
|
|
56
|
+
/** Pull the first owner-looking field from a parsed JSON body, searching by KNOWN field name only,
|
|
57
|
+
* breadth-first to a bounded depth. We NEVER guess an owner from an unnamed field — if no
|
|
58
|
+
* configured owner-field name is present anywhere in range, we return null, and the engine then
|
|
59
|
+
* yields needs-review (the anthem: an unfindable owner is loud, never a false clean). Breadth-first
|
|
60
|
+
* + shallow bound means the SHALLOWEST match wins (the real owner), not a deep look-alike. */
|
|
61
|
+
export declare function declaredOwner(json: unknown, ownerFields: string[]): string | null;
|
|
62
|
+
export interface GraphqlOp {
|
|
63
|
+
field: string;
|
|
64
|
+
objectId: string;
|
|
65
|
+
isMutation: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** Parse a GraphQL request body into a single object operation + its id, or null. Reads the id from
|
|
68
|
+
* `variables` (the standard pattern) when the query references `$var`, else from an inline literal.
|
|
69
|
+
* Returns null for anything ambiguous (no id, multiple object ops, batched array) so we stay loud. */
|
|
70
|
+
export declare function parseGraphqlOp(body: unknown): GraphqlOp | null;
|
|
71
|
+
/**
|
|
72
|
+
* Wrap a Next.js App Router route handler so BoLD watches it live. The handler runs exactly as
|
|
73
|
+
* before; its response is returned untouched. BoLD observes metadata out of band.
|
|
74
|
+
*/
|
|
75
|
+
export declare function withBold<C>(handler: RouteHandler<C>, config?: BoldConfig): RouteHandler<C>;
|
|
76
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;yFACqF;IACrF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;8BAK0B;IAC1B,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChF;AAED;0EAC0E;AAC1E,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAsE7F,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAMjG;AAOD;;;;+FAI+F;AAC/F,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,IAAI,CAuBjF;AAeD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;uGAEuG;AACvG,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAyB9D;AAiGD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,CAAC,CAAC,CA4B1F"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @boldsec/next: the Next.js (App Router) route wrapper for BoLD live monitoring (Stage 3 / S3.3).
|
|
3
|
+
*
|
|
4
|
+
* Why a route wrapper and not `proxy.ts`/middleware: Next.js proxy runs BEFORE the response and
|
|
5
|
+
* cannot read the response body — but confirming a BOLA needs the owner field FROM the response
|
|
6
|
+
* (e.g. `ownerId`). The route handler is the only place that sees both the request and the response,
|
|
7
|
+
* so that is where BoLD watches. The user wraps their existing handler; it runs unchanged.
|
|
8
|
+
*
|
|
9
|
+
* What it sends to BoLD: METADATA ONLY — who made the request (an opaque, non-reversible identity
|
|
10
|
+
* handle, never the token/cookie), the endpoint shape, the object id, the status, and the object's
|
|
11
|
+
* declared owner if the response JSON exposed one. The response body is read once to extract that
|
|
12
|
+
* single owner field and then DISCARDED. No body, no headers, no credentials ever leave the app.
|
|
13
|
+
*
|
|
14
|
+
* Failure mode: FAIL-SAFE. Shipping the metadata is fire-and-forget; if BoLD is slow or down, the
|
|
15
|
+
* user's response is returned untouched and on time. BoLD can never break or slow the app it watches.
|
|
16
|
+
*
|
|
17
|
+
* Usage (a few lines, no terminal):
|
|
18
|
+
*
|
|
19
|
+
* // app/api/invoices/[id]/route.ts
|
|
20
|
+
* import { withBold } from "@boldsec/next";
|
|
21
|
+
*
|
|
22
|
+
* export const GET = withBold(
|
|
23
|
+
* async (req, ctx) => {
|
|
24
|
+
* const invoice = await getInvoice(ctx.params.id);
|
|
25
|
+
* return Response.json(invoice); // { id, ownerId, ... }
|
|
26
|
+
* },
|
|
27
|
+
* { resolveCallerId: (req) => getUserIdFromSession(req) }, // RECOMMENDED — see BoldConfig
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* Config via env (set once): BOLD_INGEST_URL, BOLD_INGEST_KEY, optional BOLD_OWNER_FIELDS.
|
|
31
|
+
*/
|
|
32
|
+
const DEFAULT_OWNER_FIELDS = [
|
|
33
|
+
"ownerId",
|
|
34
|
+
"owner_id",
|
|
35
|
+
"userId",
|
|
36
|
+
"user_id",
|
|
37
|
+
"accountId",
|
|
38
|
+
"account_id",
|
|
39
|
+
"createdBy",
|
|
40
|
+
"created_by",
|
|
41
|
+
"tenantId",
|
|
42
|
+
"tenant_id",
|
|
43
|
+
];
|
|
44
|
+
const OBJECT_PATH = /^(.*?)\/([^/]+)\/?$/; // ".../{prefix}/{lastSegment}"
|
|
45
|
+
// An id-looking last path segment: a pure number (42), a uuid/long-hex (>=6 hex/dash chars), or a
|
|
46
|
+
// PREFIXED id with one OR MORE underscore segments (inv_7001, acct_northstar_001,
|
|
47
|
+
// note_priya_investor_followup). Requiring at least one `_segment` is what distinguishes a real id
|
|
48
|
+
// from a route word like `orders` / `login` (which must NOT be treated as an object id).
|
|
49
|
+
const ID_LIKE = /^[0-9]+$|^[0-9a-fA-F-]{6,}$|^[a-zA-Z][a-zA-Z0-9]*(?:_[a-zA-Z0-9]+)+$/;
|
|
50
|
+
function resolveConfig(cfg) {
|
|
51
|
+
const env = typeof process !== "undefined" && process.env ? process.env : {};
|
|
52
|
+
const ingestUrl = cfg?.ingestUrl ?? env.BOLD_INGEST_URL ?? "";
|
|
53
|
+
const ingestKey = cfg?.ingestKey ?? env.BOLD_INGEST_KEY ?? "";
|
|
54
|
+
if (!ingestUrl || !ingestKey)
|
|
55
|
+
return null; // not configured -> silently no-op (never throw)
|
|
56
|
+
const ownerFields = cfg?.ownerFields ??
|
|
57
|
+
(env.BOLD_OWNER_FIELDS
|
|
58
|
+
? env.BOLD_OWNER_FIELDS.split(",").map((s) => s.trim())
|
|
59
|
+
: DEFAULT_OWNER_FIELDS);
|
|
60
|
+
return { ingestUrl, ingestKey, ownerFields, resolveCallerId: cfg?.resolveCallerId };
|
|
61
|
+
}
|
|
62
|
+
/** The caller's identity for the engine. Best: the app's RESOLVED owner-id (same namespace as the
|
|
63
|
+
* response owner field) so own-access is never false-CONFIRMED. Fallback: a NON-reversible hash of
|
|
64
|
+
* the auth material (never the token) — lets BoLD tell "same vs different caller" but can't match
|
|
65
|
+
* the owner namespace. */
|
|
66
|
+
async function identityFrom(request, cfg) {
|
|
67
|
+
if (cfg.resolveCallerId) {
|
|
68
|
+
try {
|
|
69
|
+
const resolved = await cfg.resolveCallerId(request);
|
|
70
|
+
if (resolved)
|
|
71
|
+
return String(resolved);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// a throwing resolver must never break the app -> fall back to the hash
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const auth = request.headers.get("authorization") ?? request.headers.get("cookie") ?? "";
|
|
78
|
+
if (!auth)
|
|
79
|
+
return null;
|
|
80
|
+
try {
|
|
81
|
+
const data = new TextEncoder().encode(auth);
|
|
82
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
83
|
+
const hex = Array.from(new Uint8Array(digest))
|
|
84
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
85
|
+
.join("");
|
|
86
|
+
return "id_" + hex.slice(0, 16);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Split a path into an endpoint template (".../{id}") + the object id, or null if no id segment. */
|
|
93
|
+
export function endpointAndObject(pathname) {
|
|
94
|
+
const m = pathname.match(OBJECT_PATH);
|
|
95
|
+
if (!m)
|
|
96
|
+
return { endpoint: pathname, objectId: null };
|
|
97
|
+
const [, prefix, last] = m;
|
|
98
|
+
if (!ID_LIKE.test(last))
|
|
99
|
+
return { endpoint: pathname, objectId: null };
|
|
100
|
+
return { endpoint: `${prefix}/{id}`, objectId: last };
|
|
101
|
+
}
|
|
102
|
+
/** Max nesting depth declaredOwner walks. Real owner fields live shallow (top level, or under a
|
|
103
|
+
* `data`/`account`/`attributes` wrapper); we bound the walk so a deeply nested LOOK-ALIKE field can
|
|
104
|
+
* never be mistaken for the owner, and so a huge response can't cost unbounded work. */
|
|
105
|
+
const OWNER_MAX_DEPTH = 4;
|
|
106
|
+
/** Pull the first owner-looking field from a parsed JSON body, searching by KNOWN field name only,
|
|
107
|
+
* breadth-first to a bounded depth. We NEVER guess an owner from an unnamed field — if no
|
|
108
|
+
* configured owner-field name is present anywhere in range, we return null, and the engine then
|
|
109
|
+
* yields needs-review (the anthem: an unfindable owner is loud, never a false clean). Breadth-first
|
|
110
|
+
* + shallow bound means the SHALLOWEST match wins (the real owner), not a deep look-alike. */
|
|
111
|
+
export function declaredOwner(json, ownerFields) {
|
|
112
|
+
const fields = new Set(ownerFields);
|
|
113
|
+
// BFS so the shallowest occurrence of an owner field wins; bounded depth + visited-guard keep it
|
|
114
|
+
// safe on large/cyclic objects.
|
|
115
|
+
let level = [json];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
for (let depth = 0; depth <= OWNER_MAX_DEPTH && level.length; depth++) {
|
|
118
|
+
const next = [];
|
|
119
|
+
for (const node of level) {
|
|
120
|
+
if (!node || typeof node !== "object" || Array.isArray(node) || seen.has(node))
|
|
121
|
+
continue;
|
|
122
|
+
seen.add(node);
|
|
123
|
+
const obj = node;
|
|
124
|
+
for (const f of fields) {
|
|
125
|
+
const v = obj[f];
|
|
126
|
+
if (v !== undefined && v !== null && typeof v !== "object")
|
|
127
|
+
return String(v);
|
|
128
|
+
}
|
|
129
|
+
for (const v of Object.values(obj)) {
|
|
130
|
+
if (v && typeof v === "object")
|
|
131
|
+
next.push(v);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
level = next;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
// ── GraphQL adapter (Shape 3) ──────────────────────────────────────────────────────────────────
|
|
139
|
+
// GraphQL puts the object id in the REQUEST BODY (not the URL path) and the data under `data.<field>`
|
|
140
|
+
// in the response, so the path-based extractor finds nothing. This adapter parses the operation +
|
|
141
|
+
// id from the request and maps the owner from the response. The anthem floor holds throughout: if we
|
|
142
|
+
// cannot identify EXACTLY ONE object operation + its id, we return null and ship nothing for it —
|
|
143
|
+
// BoLD never invents an object, so it never produces a false verdict on a query it can't read.
|
|
144
|
+
const GQL_PATH = /\/graphql\/?$/i; // the conventional GraphQL endpoint
|
|
145
|
+
// A top-level selection like `order(id: $x)` or `invoice(id: "7")` — capture the field name + the id
|
|
146
|
+
// arg (variable ref `$name` or an inline string/number literal). Intentionally conservative.
|
|
147
|
+
const GQL_FIELD_WITH_ID = /\b([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*[^)]*?\bid\s*:\s*(?:\$([A-Za-z_][A-Za-z0-9_]*)|"([^"]+)"|([0-9]+))/;
|
|
148
|
+
/** Parse a GraphQL request body into a single object operation + its id, or null. Reads the id from
|
|
149
|
+
* `variables` (the standard pattern) when the query references `$var`, else from an inline literal.
|
|
150
|
+
* Returns null for anything ambiguous (no id, multiple object ops, batched array) so we stay loud. */
|
|
151
|
+
export function parseGraphqlOp(body) {
|
|
152
|
+
if (Array.isArray(body))
|
|
153
|
+
return null; // batched queries -> ambiguous, hold loud
|
|
154
|
+
if (!body || typeof body !== "object")
|
|
155
|
+
return null;
|
|
156
|
+
const b = body;
|
|
157
|
+
const query = b.query;
|
|
158
|
+
if (typeof query !== "string")
|
|
159
|
+
return null;
|
|
160
|
+
const variables = (b.variables && typeof b.variables === "object" ? b.variables : {});
|
|
161
|
+
const isMutation = /^\s*mutation\b/i.test(query) || /\bmutation\b/i.test(query.split("{")[0] ?? "");
|
|
162
|
+
const m = query.match(GQL_FIELD_WITH_ID);
|
|
163
|
+
if (!m)
|
|
164
|
+
return null;
|
|
165
|
+
const [, field, varRef, strLit, numLit] = m;
|
|
166
|
+
let objectId = null;
|
|
167
|
+
if (varRef) {
|
|
168
|
+
const v = variables[varRef];
|
|
169
|
+
if (v !== undefined && v !== null && typeof v !== "object")
|
|
170
|
+
objectId = String(v);
|
|
171
|
+
}
|
|
172
|
+
else if (strLit !== undefined) {
|
|
173
|
+
objectId = strLit;
|
|
174
|
+
}
|
|
175
|
+
else if (numLit !== undefined) {
|
|
176
|
+
objectId = numLit;
|
|
177
|
+
}
|
|
178
|
+
if (!objectId)
|
|
179
|
+
return null; // an id we can't resolve -> don't guess, hold loud
|
|
180
|
+
return { field, objectId, isMutation };
|
|
181
|
+
}
|
|
182
|
+
/** The owner for a GraphQL op: look under the response's `data.<field>` subtree (the GraphQL result
|
|
183
|
+
* shape), reusing the same name-based owner search. Null if not present -> needs-review. */
|
|
184
|
+
function graphqlOwner(json, field, ownerFields) {
|
|
185
|
+
if (!json || typeof json !== "object")
|
|
186
|
+
return null;
|
|
187
|
+
const data = json.data;
|
|
188
|
+
if (!data || typeof data !== "object")
|
|
189
|
+
return null;
|
|
190
|
+
const node = data[field];
|
|
191
|
+
if (node === undefined)
|
|
192
|
+
return null;
|
|
193
|
+
return declaredOwner(node, ownerFields);
|
|
194
|
+
}
|
|
195
|
+
function methodToAction(method) {
|
|
196
|
+
const m = method.toUpperCase();
|
|
197
|
+
if (m === "GET")
|
|
198
|
+
return "GET";
|
|
199
|
+
if (m === "PUT" || m === "PATCH" || m === "POST")
|
|
200
|
+
return m;
|
|
201
|
+
if (m === "DELETE")
|
|
202
|
+
return "DELETE";
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
/** Build the metadata event + ship it fire-and-forget. Never throws; never blocks the response. */
|
|
206
|
+
async function observe(request, response, cfg,
|
|
207
|
+
// The GraphQL request body, captured BEFORE the handler ran (the handler consumes the body, so a
|
|
208
|
+
// clone taken afterwards is empty — only the up-front capture in withBold has the query). null for
|
|
209
|
+
// non-GraphQL requests or when the body could not be read.
|
|
210
|
+
gqlBody) {
|
|
211
|
+
try {
|
|
212
|
+
const url = new URL(request.url);
|
|
213
|
+
const respIsJson = (response.headers.get("content-type") ?? "").includes("application/json");
|
|
214
|
+
// Resolve {endpoint, objectId, owner, method} for either a REST object route OR a GraphQL op.
|
|
215
|
+
let endpoint;
|
|
216
|
+
let objectId;
|
|
217
|
+
let owner = null;
|
|
218
|
+
let method;
|
|
219
|
+
if (request.method.toUpperCase() === "POST" && GQL_PATH.test(url.pathname)) {
|
|
220
|
+
// ── GraphQL path: the id is in the request body (captured up-front), owner under data.<field>.
|
|
221
|
+
const op = parseGraphqlOp(gqlBody);
|
|
222
|
+
if (!op)
|
|
223
|
+
return; // can't identify exactly one object op -> ship nothing, never guess
|
|
224
|
+
endpoint = `${url.pathname}#${op.field}`; // endpoint template names the GraphQL field
|
|
225
|
+
objectId = op.objectId;
|
|
226
|
+
method = op.isMutation ? "PATCH" : "GET"; // mutation -> write (UPDATE), query -> read
|
|
227
|
+
if (respIsJson) {
|
|
228
|
+
try {
|
|
229
|
+
owner = graphqlOwner(await response.clone().json(), op.field, cfg.ownerFields);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
owner = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// ── REST path: id in the URL, owner anywhere named in the response JSON. ──
|
|
238
|
+
const eo = endpointAndObject(url.pathname);
|
|
239
|
+
endpoint = eo.endpoint;
|
|
240
|
+
objectId = eo.objectId;
|
|
241
|
+
if (!objectId)
|
|
242
|
+
return; // not an object-by-id route -> nothing to judge
|
|
243
|
+
if (!methodToAction(request.method))
|
|
244
|
+
return;
|
|
245
|
+
method = request.method.toUpperCase();
|
|
246
|
+
if (respIsJson) {
|
|
247
|
+
try {
|
|
248
|
+
owner = declaredOwner(await response.clone().json(), cfg.ownerFields);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
owner = null; // not the JSON shape we need -> no owner signal, never an error
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const event = {
|
|
256
|
+
identity: await identityFrom(request, cfg),
|
|
257
|
+
method,
|
|
258
|
+
endpoint,
|
|
259
|
+
object_id: objectId,
|
|
260
|
+
status_code: response.status,
|
|
261
|
+
declared_owner: owner,
|
|
262
|
+
};
|
|
263
|
+
// On serverless (Vercel/Lambda) the function is FROZEN the instant the response is returned, so
|
|
264
|
+
// an un-awaited fetch is killed mid-flight and the metadata never leaves (proven on a real
|
|
265
|
+
// deploy — `keepalive` is a browser-only hint Node ignores). So we AWAIT the POST. Still
|
|
266
|
+
// fail-safe: the inner .catch + the outer try/catch mean a slow/down BoLD can never break or
|
|
267
|
+
// alter the response — and `observe()` itself is awaited by the wrapper only up to here, so the
|
|
268
|
+
// owner-bytes were already read from the clone before this.
|
|
269
|
+
await fetch(cfg.ingestUrl, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${cfg.ingestKey}` },
|
|
272
|
+
body: JSON.stringify(event),
|
|
273
|
+
}).catch(() => { });
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// The wrapper must NEVER affect the app. Swallow everything.
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Wrap a Next.js App Router route handler so BoLD watches it live. The handler runs exactly as
|
|
281
|
+
* before; its response is returned untouched. BoLD observes metadata out of band.
|
|
282
|
+
*/
|
|
283
|
+
export function withBold(handler, config) {
|
|
284
|
+
return async (request, context) => {
|
|
285
|
+
const cfg = resolveConfig(config);
|
|
286
|
+
// For a GraphQL POST, the id lives in the request body — and the handler will CONSUME that body
|
|
287
|
+
// when it reads it. So we must capture it from a CLONE *before* the handler runs; a clone taken
|
|
288
|
+
// afterwards is empty. We read it fail-safe: any error leaves gqlBody null and the request is
|
|
289
|
+
// simply not judged (never breaks the handler, which gets the original body untouched).
|
|
290
|
+
let gqlBody = null;
|
|
291
|
+
if (cfg && request.method.toUpperCase() === "POST") {
|
|
292
|
+
try {
|
|
293
|
+
const url = new URL(request.url);
|
|
294
|
+
if (GQL_PATH.test(url.pathname))
|
|
295
|
+
gqlBody = await request.clone().json();
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
gqlBody = null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const response = await handler(request, context);
|
|
302
|
+
if (cfg) {
|
|
303
|
+
// AWAIT the observe so the metadata POST actually transmits before the serverless function
|
|
304
|
+
// freezes at return (an un-awaited POST is silently dropped on Vercel/Lambda — proven on a
|
|
305
|
+
// real deploy). observe() is fully fail-safe internally (try/catch + .catch), so awaiting it
|
|
306
|
+
// can never throw, never alter the response, and only briefly delays returning it.
|
|
307
|
+
await observe(request, response, cfg, gqlBody);
|
|
308
|
+
}
|
|
309
|
+
return response;
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boldsec/next",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "BoLD live BOLA/IDOR monitoring for Next.js App Router: wrap a route, watch it for cross-user access. Metadata only, fail-safe.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "README.md"],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://boldsec.io",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/Sahith59/BoLD.git",
|
|
23
|
+
"directory": "web/sdk/bold-next"
|
|
24
|
+
},
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
29
|
+
"test": "node --test src/index.test.ts",
|
|
30
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"bola",
|
|
34
|
+
"idor",
|
|
35
|
+
"security",
|
|
36
|
+
"nextjs",
|
|
37
|
+
"authorization",
|
|
38
|
+
"monitoring",
|
|
39
|
+
"appsec"
|
|
40
|
+
],
|
|
41
|
+
"license": "UNLICENSED",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"next": ">=13"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^20",
|
|
50
|
+
"typescript": "^5"
|
|
51
|
+
}
|
|
52
|
+
}
|