Implement rate limiting for sign requests (#1562)
* feat: implement rate limiting for sign requests to prevent abuse * feat: add tests for rate limit handling for signing requests
This commit is contained in:
parent
4d4d0541d2
commit
4d14de52a2
@ -22,6 +22,8 @@ import Extension from './Extension.js';
|
||||
import State from './State.js';
|
||||
import Tabs from './Tabs.js';
|
||||
|
||||
const sleep = () => new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
describe('Extension', () => {
|
||||
let extension: Extension;
|
||||
let state: State;
|
||||
@ -228,7 +230,24 @@ describe('Extension', () => {
|
||||
expect(res).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw error for rate-limited signing requests', async () => {
|
||||
const registry = new TypeRegistry();
|
||||
|
||||
registry.setSignedExtensions(payload.signedExtensions);
|
||||
|
||||
await expect(
|
||||
tabs.handle(
|
||||
'1615191860871.5',
|
||||
'pub(extrinsic.sign)',
|
||||
payload,
|
||||
'http://localhost:3000',
|
||||
{} as chrome.runtime.Port
|
||||
)
|
||||
).rejects.toThrow('Rate limit exceeded. Try again later.');
|
||||
});
|
||||
|
||||
it('signs with default signed extensions - ethereum', async () => {
|
||||
await sleep();
|
||||
const ethAddress = await createAccount('ethereum');
|
||||
const ethPair = keyring.getPair(ethAddress);
|
||||
|
||||
@ -279,6 +298,7 @@ describe('Extension', () => {
|
||||
});
|
||||
|
||||
it('signs with user extensions, known types', async () => {
|
||||
await sleep();
|
||||
const types = {} as unknown as Record<string, string>;
|
||||
|
||||
const userExtensions = {
|
||||
@ -345,6 +365,7 @@ describe('Extension', () => {
|
||||
});
|
||||
|
||||
it('override default signed extension', async () => {
|
||||
await sleep();
|
||||
const types = {
|
||||
FeeExchangeV1: {
|
||||
assetId: 'Compact<AssetId>',
|
||||
@ -405,6 +426,7 @@ describe('Extension', () => {
|
||||
});
|
||||
|
||||
it('signs with user extensions, additional types', async () => {
|
||||
await sleep();
|
||||
const types = {
|
||||
myCustomType: {
|
||||
feeExchange: 'Compact<AssetId>',
|
||||
|
||||
@ -128,6 +128,10 @@ async function extractMetadata (store: MetadataStore): Promise<void> {
|
||||
export default class State {
|
||||
#authUrls = new Map<string, AuthUrlInfo>();
|
||||
|
||||
#lastRequestTimestamps = new Map<string, number>();
|
||||
#maxEntries = 10;
|
||||
#rateLimitInterval = 3000; // 3 seconds
|
||||
|
||||
readonly #authRequests: Record<string, AuthRequest> = {};
|
||||
|
||||
readonly #metaStore = new MetadataStore();
|
||||
@ -615,9 +619,33 @@ export default class State {
|
||||
return true;
|
||||
}
|
||||
|
||||
private handleSignRequest (origin: string) {
|
||||
const now = Date.now();
|
||||
const lastTime = this.#lastRequestTimestamps.get(origin) || 0;
|
||||
|
||||
if (now - lastTime < this.#rateLimitInterval) {
|
||||
throw new Error('Rate limit exceeded. Try again later.');
|
||||
}
|
||||
|
||||
// If we're about to exceed max entries, evict the oldest
|
||||
if (!this.#lastRequestTimestamps.has(origin) && this.#lastRequestTimestamps.size >= this.#maxEntries) {
|
||||
const oldestKey = this.#lastRequestTimestamps.keys().next().value;
|
||||
|
||||
oldestKey && this.#lastRequestTimestamps.delete(oldestKey);
|
||||
}
|
||||
|
||||
this.#lastRequestTimestamps.set(origin, now);
|
||||
}
|
||||
|
||||
public sign (url: string, request: RequestSign, account: AccountJson): Promise<ResponseSigning> {
|
||||
const id = getId();
|
||||
|
||||
try {
|
||||
this.handleSignRequest(url);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject): void => {
|
||||
this.#signRequests[id] = {
|
||||
...this.signComplete(id, resolve, reject),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user