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:
Arjun Porwal 2025-06-23 19:02:45 +05:30 committed by GitHub
parent 4d4d0541d2
commit 4d14de52a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 0 deletions

View File

@ -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>',

View File

@ -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),