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
Feature: SQL Injection Prevention (Flow Simulation) #23
Conversation
ebc253e
to
bbcd1c2
Compare
1a337d6
to
e2fe9a4
Compare
- add prepare parameters method in data source interface. - inject data source in route generator - refactor template engine for re-wrap data to context object. - update req tag runner to get bind paramters from context and send to executor for creating builder. - add identifier in data query builder - update test cases for data query builder and template engine. - update route generator, app, server test cases.
e2fe9a4
to
90b3942
Compare
Rebase finished from #25. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other part LGTM.
Just a kindly reminder: We have a tradeoff when using prepared parameters: we won’t able to take operation on parameters anymore.
select * from users where age > {{ context.params.age + 20 }}
--
age=18
select * from users where age > 38
// handle parameterized query statement | ||
let query = statement; | ||
for (const identifier of Object.keys(bindParams)) { | ||
query = query.replace(identifier, bindParams[identifier]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use global searching instead:
query = query.replace(identifier, bindParams[identifier]); | |
query = query.replace(new RegExp(identifier, 'g') , bindParams[identifier]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reviewing! it has been fixed.
@@ -75,6 +78,7 @@ describe('Test data query builder > where clause', () => { | |||
const builder = new DataQueryBuilder({ | |||
statement: 'select * from orders', | |||
dataSource: stubDataSource, | |||
bindParams: sinon.stubInterface<BindParameters>(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: I'd prefer to create a helper function like createMockBuild
to prevent potential changes in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting it! it has been refactored.
@@ -304,6 +310,7 @@ describe('Test data query builder > having clause', () => { | |||
const builder = new DataQueryBuilder({ | |||
statement: 'select * from orders', | |||
dataSource: stubDataSource, | |||
bindParams: sinon.stubInterface<BindParameters>(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: We used to stub at the arranging phase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting it! it has been refactored, I use the createStub
function like above you suggested to handle it.
const binds = (context.ctx?.context || {})['_paramBinds'] || {}; | ||
const builder = await this.executor.createBuilder(query, binds); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding a testing case to cover your change:
// Arrange
const { compiler, loader, builder, executor } = await createTestCompiler();
const { compiledData } = await compiler.compile(`
{% req userCount main %}
select count(*) as count from user where user.id = '{{ context.params.userId }}';
{% endreq %}
`);
builder.value.onFirstCall().resolves([{ count: 1 }]);
// Action
loader.setSource('test', compiledData);
await compiler.execute('test', {
context: { params: { userId: '@userId' } },
['_paramBinds']: { '@userId': '000000' },
});
// Assert
expect(executor.createBuilder.firstCall.args[0]).toBe(
`select count(*) as count from user where user.id = '@userId';`
);
expect(executor.createBuilder.firstCall.args[1]).toEqual({
'@userId': '000000',
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting and providing the complete test cases ❤️
return this.compiler.execute( | ||
templateName, | ||
{ | ||
context: { | ||
...others, | ||
['params']: prepared?.identifiers || {}, | ||
['_paramBinds']: prepared?.binds || {}, | ||
}, | ||
}, | ||
pagination | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't want users to access _paramBinds via templates, how about moving it out of context
properties?
return this.compiler.execute( | |
templateName, | |
{ | |
context: { | |
...others, | |
['params']: prepared?.identifiers || {}, | |
['_paramBinds']: prepared?.binds || {}, | |
}, | |
}, | |
pagination | |
); | |
return this.compiler.execute( | |
templateName, | |
{ | |
context: { | |
...others, | |
['params']: prepared?.identifiers || {}, | |
}, | |
['_paramBinds']: prepared?.binds || {}, | |
}, | |
pagination | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting, you're right, it will be better to move out of context, it has been fixed!
@@ -37,11 +38,13 @@ export class RouteGenerator { | |||
@inject(TYPES.RequestValidator) reqValidator: IRequestValidator, | |||
@inject(TYPES.PaginationTransformer) | |||
paginationTransformer: IPaginationTransformer, | |||
@inject(CORE_TYPES.DataSource) dataSource: DataSource, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT: We have more and more dependencies to inject, I think it is better to add RestfulRoute and GraphQLRoute to container and inject them to RouteGenerator directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for suggesting, I agree with your idea, but it needs some time to refactor ( maybe need to use AutoNamedFactory
), I would like to refactor it with a new feature together :)
Thanks for reminding me~, I think we could solve the case in the future with new design ? |
Hi @oscar60310 besides the last NIT I would like to handle it in the new feature (need some time to refactor and test), others are fixed, thanks so much! |
Description
Support preventing sql injection attack.
For the detailed description and introduction, please read Notion link !
🔔 Notice: The PR is only to simulates the getting data source and re-parsing parameters flow and add re-parse method in the interface, the really logistic should wait data source to implement, then could be able to replace it.
How To Test / Expected Results
For the test result, please see the below test cases that passed the unit test:
Core package
Serve package
Integration testing package
Commit Message