Skip to content

Commit

Permalink
fix!: Return ERROR decision when fingerprint cannot be generated (#1990)
Browse files Browse the repository at this point in the history
This changes the SDK to return an `ERROR` decision if we cannot build the fingerprint based on the data provided to `protect()`.  This is a breaking change because we used to continue with an empty fingerprint, but that wasn't the correct behavior.

I've also regenerated the Analyze Wasm and we've removed the logging in an attempt to check performance/memory usage which we'll investigate with examples and users. Ref #1724

Closes #1801
  • Loading branch information
blaine-arcjet authored Oct 22, 2024
1 parent 3da543e commit 618a1ee
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 79 deletions.
8 changes: 0 additions & 8 deletions analyze/edge-light.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ async function init(
}

const coreImports: ImportObject = {
"arcjet:js-req/logger": {
debug(msg) {
log.debug(msg);
},
error(msg) {
log.error(msg);
},
},
"arcjet:js-req/email-validator-overrides": {
isFreeEmail(domain) {
if (FREE_EMAIL_PROVIDERS.includes(domain)) {
Expand Down
8 changes: 0 additions & 8 deletions analyze/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,6 @@ async function init(
}

const coreImports: ImportObject = {
"arcjet:js-req/logger": {
debug(msg) {
log.debug(msg);
},
error(msg) {
log.error(msg);
},
},
"arcjet:js-req/email-validator-overrides": {
isFreeEmail(domain) {
if (FREE_EMAIL_PROVIDERS.includes(domain)) {
Expand Down
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core2.wasm
Binary file not shown.
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.core3.wasm
Binary file not shown.
3 changes: 1 addition & 2 deletions analyze/wasm/arcjet_analyze_js_req.component.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,15 @@ export interface BotResult {
denied: Array<BotEntity>,
}
import { ArcjetJsReqEmailValidatorOverrides } from './interfaces/arcjet-js-req-email-validator-overrides.js';
import { ArcjetJsReqLogger } from './interfaces/arcjet-js-req-logger.js';
import { ArcjetJsReqSensitiveInformationIdentifier } from './interfaces/arcjet-js-req-sensitive-information-identifier.js';
export interface ImportObject {
'arcjet:js-req/email-validator-overrides': typeof ArcjetJsReqEmailValidatorOverrides,
'arcjet:js-req/logger': typeof ArcjetJsReqLogger,
'arcjet:js-req/sensitive-information-identifier': typeof ArcjetJsReqSensitiveInformationIdentifier,
}
export interface Root {
detectBot(request: string, options: BotConfig): BotResult,
generateFingerprint(request: string, characteristics: Array<string>): string,
validateCharacteristics(request: string, characteristics: Array<string>): void,
isValidEmail(candidate: string, options: EmailValidationConfig): EmailValidationResult,
detectSensitiveInfo(content: string, options: SensitiveInfoConfig): SensitiveInfoResult,
}
Expand Down
132 changes: 92 additions & 40 deletions analyze/wasm/arcjet_analyze_js_req.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
const module2 = getCoreModule('arcjet_analyze_js_req.component.core3.wasm');

const { hasGravatar, hasMxRecords, isDisposableEmail, isFreeEmail } = imports['arcjet:js-req/email-validator-overrides'];
const { debug, error } = imports['arcjet:js-req/logger'];
const { detect } = imports['arcjet:js-req/sensitive-information-identifier'];
let gen = (function* init () {
let exports0;
Expand All @@ -48,20 +47,6 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
let realloc0;

function trampoline0(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
debug(result0);
}

function trampoline1(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
error(result0);
}

function trampoline2(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
Expand Down Expand Up @@ -92,7 +77,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
return enum1;
}

function trampoline3(arg0, arg1) {
function trampoline1(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
Expand Down Expand Up @@ -123,7 +108,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
return enum1;
}

function trampoline4(arg0, arg1) {
function trampoline2(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
Expand Down Expand Up @@ -154,7 +139,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
return enum1;
}

function trampoline5(arg0, arg1) {
function trampoline3(arg0, arg1) {
var ptr0 = arg0;
var len0 = arg1;
var result0 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr0, len0));
Expand Down Expand Up @@ -185,7 +170,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
return enum1;
}

function trampoline6(arg0, arg1, arg2) {
function trampoline4(arg0, arg1, arg2) {
var len1 = arg1;
var base1 = arg0;
var result1 = [];
Expand Down Expand Up @@ -248,21 +233,18 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
let postReturn1;
let postReturn2;
let postReturn3;
let postReturn4;
Promise.all([module0, module1, module2]).catch(() => {});
({ exports: exports0 } = yield instantiateCore(yield module1));
({ exports: exports1 } = yield instantiateCore(yield module0, {
'arcjet:js-req/email-validator-overrides': {
'has-gravatar': exports0['5'],
'has-mx-records': exports0['4'],
'is-disposable-email': exports0['3'],
'is-free-email': exports0['2'],
},
'arcjet:js-req/logger': {
debug: exports0['0'],
error: exports0['1'],
'has-gravatar': exports0['3'],
'has-mx-records': exports0['2'],
'is-disposable-email': exports0['1'],
'is-free-email': exports0['0'],
},
'arcjet:js-req/sensitive-information-identifier': {
detect: exports0['6'],
detect: exports0['4'],
},
}));
memory0 = exports1.memory;
Expand All @@ -275,14 +257,13 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
'2': trampoline2,
'3': trampoline3,
'4': trampoline4,
'5': trampoline5,
'6': trampoline6,
},
}));
postReturn0 = exports1['cabi_post_detect-bot'];
postReturn1 = exports1['cabi_post_generate-fingerprint'];
postReturn2 = exports1['cabi_post_is-valid-email'];
postReturn3 = exports1['cabi_post_detect-sensitive-info'];
postReturn2 = exports1['cabi_post_validate-characteristics'];
postReturn3 = exports1['cabi_post_is-valid-email'];
postReturn4 = exports1['cabi_post_detect-sensitive-info'];

function detectBot(arg0, arg1) {
var ptr0 = utf8Encode(arg0, realloc0, memory0);
Expand Down Expand Up @@ -404,12 +385,83 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
dataView(memory0).setInt32(base + 0, ptr1, true);
}
const ret = exports1['generate-fingerprint'](ptr0, len0, result2, len2);
var ptr3 = dataView(memory0).getInt32(ret + 0, true);
var len3 = dataView(memory0).getInt32(ret + 4, true);
var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3));
const retVal = result3;
let variant5;
switch (dataView(memory0).getUint8(ret + 0, true)) {
case 0: {
var ptr3 = dataView(memory0).getInt32(ret + 4, true);
var len3 = dataView(memory0).getInt32(ret + 8, true);
var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3));
variant5= {
tag: 'ok',
val: result3
};
break;
}
case 1: {
var ptr4 = dataView(memory0).getInt32(ret + 4, true);
var len4 = dataView(memory0).getInt32(ret + 8, true);
var result4 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr4, len4));
variant5= {
tag: 'err',
val: result4
};
break;
}
default: {
throw new TypeError('invalid variant discriminant for expected');
}
}
const retVal = variant5;
postReturn1(ret);
return retVal;
if (typeof retVal === 'object' && retVal.tag === 'err') {
throw new ComponentError(retVal.val);
}
return retVal.val;
}

function validateCharacteristics(arg0, arg1) {
var ptr0 = utf8Encode(arg0, realloc0, memory0);
var len0 = utf8EncodedLen;
var vec2 = arg1;
var len2 = vec2.length;
var result2 = realloc0(0, 0, 4, len2 * 8);
for (let i = 0; i < vec2.length; i++) {
const e = vec2[i];
const base = result2 + i * 8;var ptr1 = utf8Encode(e, realloc0, memory0);
var len1 = utf8EncodedLen;
dataView(memory0).setInt32(base + 4, len1, true);
dataView(memory0).setInt32(base + 0, ptr1, true);
}
const ret = exports1['validate-characteristics'](ptr0, len0, result2, len2);
let variant4;
switch (dataView(memory0).getUint8(ret + 0, true)) {
case 0: {
variant4= {
tag: 'ok',
val: undefined
};
break;
}
case 1: {
var ptr3 = dataView(memory0).getInt32(ret + 4, true);
var len3 = dataView(memory0).getInt32(ret + 8, true);
var result3 = utf8Decoder.decode(new Uint8Array(memory0.buffer, ptr3, len3));
variant4= {
tag: 'err',
val: result3
};
break;
}
default: {
throw new TypeError('invalid variant discriminant for expected');
}
}
const retVal = variant4;
postReturn2(ret);
if (typeof retVal === 'object' && retVal.tag === 'err') {
throw new ComponentError(retVal.val);
}
return retVal.val;
}

function isValidEmail(arg0, arg1) {
Expand Down Expand Up @@ -478,7 +530,7 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
}
}
const retVal = variant8;
postReturn2(ret);
postReturn3(ret);
if (typeof retVal === 'object' && retVal.tag === 'err') {
throw new ComponentError(retVal.val);
}
Expand Down Expand Up @@ -704,11 +756,11 @@ function instantiate(getCoreModule, imports, instantiateCore = WebAssembly.insta
allowed: result12,
denied: result15,
};
postReturn3(ret);
postReturn4(ret);
return retVal;
}

return { detectBot, detectSensitiveInfo, generateFingerprint, isValidEmail, };
return { detectBot, detectSensitiveInfo, generateFingerprint, isValidEmail, validateCharacteristics, };
})();
let promise, resolve, reject;
function runNext (value) {
Expand Down
Binary file modified analyze/wasm/arcjet_analyze_js_req.component.wasm
Binary file not shown.
8 changes: 0 additions & 8 deletions analyze/workerd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ async function init(
}

const coreImports: ImportObject = {
"arcjet:js-req/logger": {
debug(msg) {
log.debug(msg);
},
error(msg) {
log.error(msg);
},
},
"arcjet:js-req/email-validator-overrides": {
isFreeEmail(domain) {
if (FREE_EMAIL_PROVIDERS.includes(domain)) {
Expand Down
36 changes: 29 additions & 7 deletions arcjet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1246,13 +1246,35 @@ export default function arcjet<
...ctx,
};

log.time?.("fingerprint");
const fingerprint = await analyze.generateFingerprint(
baseContext,
toAnalyzeRequest(details),
);
log.debug("fingerprint (%s): %s", rt, fingerprint);
log.timeEnd?.("fingerprint");
let fingerprint = "";
try {
log.time?.("fingerprint");
fingerprint = await analyze.generateFingerprint(
baseContext,
toAnalyzeRequest(details),
);
log.debug("fingerprint (%s): %s", rt, fingerprint);
} catch (error) {
log.error(
{ error },
"Failed to build fingerprint. Please verify your Characteristics.",
);

const decision = new ArcjetErrorDecision({
ttl: 0,
reason: new ArcjetErrorReason(
`Failed to build fingerprint - ${errorMessage(error)}`,
),
// No results because we couldn't create a fingerprint
results: [],
});

// TODO: Consider sending this to Report when we have an infallible fingerprint

return decision;
} finally {
log.timeEnd?.("fingerprint");
}

const context: ArcjetContext = Object.freeze({
...baseContext,
Expand Down
2 changes: 1 addition & 1 deletion arcjet/test/index.edge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("Arcjet: Env = Edge runtime", () => {
abc: 123,
requested: 1,
email: "",
ip: "",
ip: "100.100.100.100",
method: "",
protocol: "",
host: "",
Expand Down
12 changes: 7 additions & 5 deletions arcjet/test/index.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2623,7 +2623,7 @@ describe("SDK", () => {
expect(denied.protect).toHaveBeenCalledTimes(1);
});

test("works with an empty request object", async () => {
test("returns an ERROR decision if fingerprint cannot be generated", async () => {
const client = {
decide: jest.fn(async () => {
return new ArcjetAllowDecision({
Expand All @@ -2649,10 +2649,10 @@ describe("SDK", () => {
};

const decision = await aj.protect(context, request);
expect(decision.conclusion).toEqual("ALLOW");
expect(decision.conclusion).toEqual("ERROR");
});

test("does not crash with no request object", async () => {
test("returns an ERROR decision with no request object", async () => {
const client = {
decide: jest.fn(async () => {
return new ArcjetAllowDecision({
Expand All @@ -2673,7 +2673,7 @@ describe("SDK", () => {

// @ts-expect-error
const decision = await aj.protect();
expect(decision.conclusion).toEqual("ALLOW");
expect(decision.conclusion).toEqual("ERROR");
});

test("returns an ERROR decision when more than 10 rules are generated", async () => {
Expand All @@ -2688,7 +2688,9 @@ describe("SDK", () => {
report: jest.fn(),
};

const request = {};
const request = {
ip: "100.100.100.100",
};

const rules: ArcjetRule[][] = [];
// We only iterate 4 times because `testRuleMultiple` generates 3 rules
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified redact-wasm/wasm/arcjet_analyze_bindings_redact.component.wasm
Binary file not shown.

0 comments on commit 618a1ee

Please sign in to comment.