Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(website): update deployer subnav #1615

Merged
merged 20 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/debug": "^4.1.12",
"@types/deep-freeze": "^0.1.5",
"@types/jest": "^29.5.12",
"@types/json-stable-stringify": "^1.1.0",
"@types/lodash": "^4.17.7",
Expand All @@ -48,16 +49,20 @@
"dependencies": {
"@usecannon/router": "^4.1.2",
"@usecannon/web-solc": "0.5.1",
"acorn": "^8.14.0",
"axios": "^1.7.2",
"axios-retry": "^4.4.2",
"buffer": "^6.0.3",
"chalk": "^4.1.2",
"debug": "^4.3.6",
"deep-freeze": "^0.0.1",
"form-data": "^4.0.0",
"fuse.js": "^7.0.0",
"lodash": "^4.17.21",
"pako": "^2.1.0",
"promise-events": "^0.2.4",
"rfdc": "^1.4.1",
"ses": "^1.10.0",
"typestub-ipfs-only-hash": "^4.0.0",
"viem": "^2.21.15",
"zod": "^3.23.6"
Expand Down
151 changes: 148 additions & 3 deletions packages/builder/src/access-recorder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,75 @@ import { computeTemplateAccesses } from './access-recorder';

describe('access-recorder.ts', () => {
describe('computeTemplateAccesses()', () => {
it('computes dependency with addition operation', () => {
expect(computeTemplateAccesses('<%= settings.value1 + settings.value2 %>')).toEqual({
accesses: ['settings.value1', 'settings.value2'],
unableToCompute: false,
});
});

it('computes simple addition', () => {
expect(computeTemplateAccesses('<%= 1 + 1 %>')).toEqual({
accesses: [],
unableToCompute: false,
});
});

it('computes dependency with subtraction operation', () => {
expect(computeTemplateAccesses('<%= settings.value1 - settings.value2 %>')).toEqual({
accesses: ['settings.value1', 'settings.value2'],
unableToCompute: false,
});
});

it('computes dependency with multiplication operation', () => {
expect(computeTemplateAccesses('<%= settings.value1 * settings.value2 %>')).toEqual({
accesses: ['settings.value1', 'settings.value2'],
unableToCompute: false,
});
});

it('computes dependency with division operation', () => {
expect(computeTemplateAccesses('<%= settings.value1 / settings.value2 %>')).toEqual({
accesses: ['settings.value1', 'settings.value2'],
unableToCompute: false,
});
});

it('computes dependency with complex math operation', () => {
expect(
computeTemplateAccesses('<%= (settings.value1 + settings.value2) * settings.value3 / settings.value4 %>')
).toEqual({
accesses: ['settings.value1', 'settings.value2', 'settings.value3', 'settings.value4'],
unableToCompute: false,
});
});

it('computes multiple dependencies on different template tags', () => {
expect(computeTemplateAccesses('<%= settings.woot %>-<%= settings.woot2 %>')).toEqual({
accesses: ['settings.woot', 'settings.woot2'],
unableToCompute: false,
});
});

it('computes simple dependency', () => {
expect(computeTemplateAccesses('<%= settings.woot %>')).toEqual({
accesses: ['settings.woot'],
unableToCompute: false,
});
});

it('computes array dependency', () => {
expect(
computeTemplateAccesses(
'["<%= settings.camelotSwapPublisherAdmin1 %>","<%= settings.camelotSwapPublisherAdmin2 %>"]'
)
).toEqual({
accesses: ['settings.camelotSwapPublisherAdmin1', 'settings.camelotSwapPublisherAdmin2'],
unableToCompute: false,
});
});

it('computes dependency using simple CannonHelperContext', () => {
expect(computeTemplateAccesses('<%= parseEther(settings.woot) %>')).toEqual({
accesses: ['settings.woot'],
Expand All @@ -26,10 +88,93 @@ describe('access-recorder.ts', () => {
unableToCompute: false,
});
});
});

describe('computeTemplateAccesses() syntax validation', () => {
it('handles invalid template syntax - unmatched brackets', () => {
expect(computeTemplateAccesses('<%= settings.value) %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('handles empty template tags', () => {
expect(computeTemplateAccesses('<%=%>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('handles multiple template tags with mixed validity', () => {
expect(computeTemplateAccesses('<%= settings.valid %> and <% invalid.syntax')).toEqual({
accesses: ['settings.valid'],
unableToCompute: false,
});
});

it('handles template with only whitespace', () => {
expect(computeTemplateAccesses('<%= %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});
});

describe('computeTemplateAccesses() security', () => {
it('prevents direct code execution', () => {
expect(computeTemplateAccesses('<%= process.exit(1) %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents access to global objects', () => {
expect(computeTemplateAccesses('<%= global.process %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents require/import statements', () => {
expect(computeTemplateAccesses('<%= require("fs") %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents eval usage', () => {
expect(computeTemplateAccesses('<%= eval("console.log(\'REKT\')") %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents Function constructor usage', () => {
expect(computeTemplateAccesses('<%= new Function("return process")() %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents setTimeout/setInterval usage', () => {
expect(computeTemplateAccesses('<%= setTimeout(() => {}, 1000) %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('prevents overriding console.log', () => {
expect(
computeTemplateAccesses('<%= console.log=function(n){require("fs").writeFileSync("./exploit.log",n)} %>')
).toEqual({
accesses: [],
unableToCompute: true,
});
});

it('recognizes whene dependencies are not found', () => {
expect(computeTemplateAccesses('<%= contracts.hello %><%= sophistication(settings.woot) %>')).toEqual({
accesses: ['contracts.hello'],
it('prevents assignment of values', () => {
expect(computeTemplateAccesses('<%= const value = 1 + 2 %>')).toEqual({
accesses: [],
unableToCompute: true,
});
});
Expand Down
122 changes: 85 additions & 37 deletions packages/builder/src/access-recorder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Debug from 'debug';
import _ from 'lodash';
import * as viem from 'viem';
import { CannonHelperContext } from './types';
import { template } from './utils/template';
import { CannonHelperContext } from './types';

const debug = Debug('cannon:builder:access-recorder');

Expand Down Expand Up @@ -32,6 +32,7 @@ class ExtendableProxy {
});
}
}

export class AccessRecorder extends ExtendableProxy {
getAccesses(depth: number, cur = 1) {
if (cur == depth) {
Expand All @@ -52,15 +53,33 @@ export class AccessRecorder extends ExtendableProxy {
return acc;
}
}
export type AccessComputationResult = { accesses: string[]; unableToCompute: boolean };

export function computeTemplateAccesses(str?: string, possibleNames: string[] = []): AccessComputationResult {
if (!str) {
return { accesses: [], unableToCompute: false };
}
export type AccessComputationResult = { accesses: string[]; unableToCompute: boolean };

type AccessRecorderMap = { [k: string]: AccessRecorder };
const recorders: { [k: string]: AccessRecorder | AccessRecorderMap } = {
type AccessRecorderMap = { [k: string]: AccessRecorder };

type TemplateContext = {
[k: string]: AccessRecorder | AccessRecorderMap | unknown;
};

/**
* Setup the template context.
* @param possibleNames - The possible names to setup the context for
* @returns The template context
*/
function setupTemplateContext(possibleNames: string[] = []): TemplateContext {
// Create a fake helper context, so the render works but no real functions are called
const fakeHelperContext = _createDeepNoopObject(CannonHelperContext);

const recorders: TemplateContext = {
// Include base context variables, no access recording as they are always available
chainId: 0,
timestamp: 0,
package: { version: '0.0.0' },
...fakeHelperContext,

// Add access recorders for the base context variables, these are the ones
// used to calculate dependencies beween steps
contracts: new AccessRecorder(),
imports: new AccessRecorder(),
extras: new AccessRecorder(),
Expand All @@ -71,43 +90,40 @@ export function computeTemplateAccesses(str?: string, possibleNames: string[] =
settings: new AccessRecorder(viem.zeroAddress),
};

for (const [n, ctxVal] of Object.entries(CannonHelperContext)) {
if (typeof ctxVal === 'function') {
// the types have been a massive unsolvableseeming pain here
recorders[n] = _.noop as unknown as AccessRecorder;
} else if (typeof ctxVal === 'object') {
for (const [key, val] of Object.entries(ctxVal)) {
if (typeof val === 'function') {
if (!recorders[n]) recorders[n] = {} as AccessRecorderMap;
(recorders[n] as AccessRecorderMap)[key] = _.noop as unknown as AccessRecorder;
} else {
recorders[n] = ctxVal as unknown as AccessRecorder;
}
}
} else {
recorders[n] = ctxVal as unknown as AccessRecorder;
}
}

// add possible names
for (const n of possibleNames) {
recorders[n] = new AccessRecorder();
}

const baseTemplate = template(str, {
imports: recorders,
});
return recorders;
}

let unableToCompute = false;
try {
baseTemplate();
} catch (err) {
debug('ran into template processing error, mark unable to compute', err);
unableToCompute = true;
export function _createDeepNoopObject<T>(obj: T): T {
if (_.isFunction(obj)) {
return _.noop as T;
}

if (Array.isArray(obj)) {
return obj.map((item) => _createDeepNoopObject(item)) as T;
}

if (_.isPlainObject(obj)) {
return _.mapValues(obj as Record<string, unknown>, (value) => _createDeepNoopObject(value)) as T;
}

return obj;
}

/**
* Collect the accesses from the recorders.
* @param recorders - The recorders to collect accesses from
* @param possibleNames - The possible names to collect accesses from
* @returns The accesses
*/
function collectAccesses(recorders: TemplateContext, possibleNames: string[]): string[] {
const accesses: string[] = [];

for (const recorder of _.difference(Object.keys(recorders), Object.keys(CannonHelperContext))) {
for (const recorder of Object.keys(recorders)) {
if (recorders[recorder] instanceof AccessRecorder) {
if (possibleNames.includes(recorder) && recorders[recorder].accessed.size > 0) {
accesses.push(recorder);
Expand All @@ -117,9 +133,41 @@ export function computeTemplateAccesses(str?: string, possibleNames: string[] =
}
}

return { accesses, unableToCompute };
return accesses;
}

/**
* Compute the accesses from the template.
* @param str - The template to compute accesses from
* @param possibleNames - The possible names to compute accesses from
* @returns The accesses
*/
export function computeTemplateAccesses(str?: string, possibleNames: string[] = []): AccessComputationResult {
if (!str) {
return { accesses: [], unableToCompute: false };
}

const recorders = setupTemplateContext(possibleNames);

try {
// we give it "true" for safeContext to avoid cloning and freezing of the object
// this is because we want to keep the access recorder, and is not a security risk
// if the user can modify that object
template(str, recorders, true);
const accesses = collectAccesses(recorders, possibleNames);
return { accesses, unableToCompute: false };
} catch (err) {
debug('Template execution failed:', err);
return { accesses: [], unableToCompute: true };
}
}

/**
* Merge two template access computation results.
* @param r1 - The first result
* @param r2 - The second result
* @returns The merged result
*/
export function mergeTemplateAccesses(r1: AccessComputationResult, r2: AccessComputationResult): AccessComputationResult {
return {
accesses: [...r1.accesses, ...r2.accesses],
Expand Down
4 changes: 2 additions & 2 deletions packages/builder/src/actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ActionKinds, registerAction, validateConfig, CannonAction } from './actions';
import { ChainArtifacts, ChainBuilderContext, ChainBuilderContextWithHelpers, ChainBuilderRuntimeInfo } from './types';
import { ChainArtifacts, ChainBuilderContext, ChainBuilderRuntimeInfo } from './types';
import { z } from 'zod';
import { PackageReference } from './package-reference';

Expand All @@ -11,7 +11,7 @@ const FakeAction: CannonAction = {
version: z.string(),
}),

async getState(_runtime: ChainBuilderRuntimeInfo, ctx: ChainBuilderContextWithHelpers, config: Record<string, unknown>) {
async getState(_runtime: ChainBuilderRuntimeInfo, ctx: ChainBuilderContext, config: Record<string, unknown>) {
return this.configInject(ctx, config, { ref: new PackageReference('hello:1.0.0'), currentLabel: '' });
},

Expand Down
Loading
Loading