Skip to content

Commit

Permalink
feat(website): update deployer subnav (#1615)
Browse files Browse the repository at this point in the history
  • Loading branch information
noahlitvin authored Jan 6, 2025
1 parent 31c7b25 commit e5b047a
Show file tree
Hide file tree
Showing 53 changed files with 2,497 additions and 1,683 deletions.
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

0 comments on commit e5b047a

Please sign in to comment.