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
Fix: Fix and enhance SQL injection prevention #40
Changes from all commits
bc94893
5f4e049
d1098cc
8aad751
4b32905
5d878aa
70d41ff
27f6232
5e9499a
5ff11a6
4285267
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,5 @@ request: | |
description: constituent id | ||
validators: | ||
- required | ||
exampleParameter: | ||
id: "1" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,5 @@ request: | |
description: constituent id | ||
validators: | ||
- required | ||
exampleParameter: | ||
id: '1' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import { PrepareParameterFunc } from '@vulcan-sql/core/models'; | ||
|
||
export class Parameterizer { | ||
private parameterIndex = 1; | ||
private sealed = false; | ||
// We MUST not use pure object here because we care about the order of the keys. | ||
// https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#description | ||
private idToValueMapping = new Map<string, any>(); | ||
private valueToIdMapping = new Map<any, string>(); | ||
private prepare: PrepareParameterFunc; | ||
|
||
constructor(prepare: PrepareParameterFunc) { | ||
this.prepare = prepare; | ||
} | ||
|
||
public async generateIdentifier(value: any): Promise<string> { | ||
if (this.sealed) | ||
throw new Error( | ||
`This parameterizer has been sealed, we might use the parameterizer from a wrong request scope.` | ||
); | ||
if (this.valueToIdMapping.has(value)) | ||
return this.valueToIdMapping.get(value)!; | ||
const id = await this.prepare({ | ||
parameterIndex: this.parameterIndex++, | ||
value, | ||
}); | ||
this.idToValueMapping.set(id, value); | ||
this.valueToIdMapping.set(value, id); | ||
return id; | ||
} | ||
|
||
public seal() { | ||
this.sealed = true; | ||
} | ||
|
||
public getBinding() { | ||
return this.idToValueMapping; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { | ||
FilterBuilder, | ||
VulcanInternalExtension, | ||
} from '@vulcan-sql/core/models'; | ||
import { RAW_FILTER_NAME } from './constants'; | ||
|
||
@VulcanInternalExtension() | ||
export class RawBuilder extends FilterBuilder { | ||
public filterName = RAW_FILTER_NAME; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { | ||
FilterRunner, | ||
FilterRunnerTransformOptions, | ||
VulcanInternalExtension, | ||
} from '@vulcan-sql/core/models'; | ||
import { RAW_FILTER_NAME } from './constants'; | ||
|
||
@VulcanInternalExtension() | ||
export class RawRunner extends FilterRunner { | ||
public filterName = RAW_FILTER_NAME; | ||
|
||
public async transform({ | ||
value, | ||
}: FilterRunnerTransformOptions): Promise<any> { | ||
// Do nothing, this filer is only a place holder to block sanitizer | ||
return value; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,8 @@ import { | |
TagRunnerOptions, | ||
VulcanInternalExtension, | ||
} from '@vulcan-sql/core/models'; | ||
import { FINIAL_BUILDER_NAME } from './constants'; | ||
import { FINIAL_BUILDER_NAME, PARAMETERIZER_VAR_NAME } from './constants'; | ||
import { Parameterizer } from './parameterizer'; | ||
|
||
@VulcanInternalExtension() | ||
export class ReqTagRunner extends TagRunner { | ||
|
@@ -23,17 +24,27 @@ export class ReqTagRunner extends TagRunner { | |
} | ||
|
||
public async run({ context, args, contentArgs }: TagRunnerOptions) { | ||
const name = args[0]; | ||
const name = String(args[0]); | ||
|
||
const parameterizer = new Parameterizer( | ||
this.executor.prepare.bind(this.executor) | ||
); | ||
// parameterizer from parent, we should set it back after rendered our context. | ||
const parentParameterizer = context.lookup(PARAMETERIZER_VAR_NAME); | ||
context.setVariable(PARAMETERIZER_VAR_NAME, parameterizer); | ||
let query = ''; | ||
for (let index = 0; index < contentArgs.length; index++) { | ||
query += await contentArgs[index](); | ||
} | ||
// Seal current parameterizer to avoid incorrect usage. | ||
parameterizer.seal(); | ||
context.setVariable(PARAMETERIZER_VAR_NAME, parentParameterizer); | ||
query = query | ||
.split(/\r?\n/) | ||
.filter((line) => line.trim().length > 0) | ||
.join('\n'); | ||
// Get bind real parameters and pass to data query builder for data source used. | ||
const binds = (context.ctx || {})['_paramBinds'] || {}; | ||
const binds = parameterizer.getBinding(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We call I'm really confused the each filter's execute order and when calling There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes! The magic happened because of the shared context. Please have a look at the image below.
We're using DFS to compile and execute the nodes, so we run req tag first, then filter. |
||
const builder = await this.executor.createBuilder(query, binds); | ||
context.setVariable(name, builder); | ||
|
||
|
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.
Sorry, I don't understand why we set the parameterizer back to our context when parameterizer from parent, could you tell more information? Thanks
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.
Because the parent and children shared the same context, if the children don't set it back, their parent will lost its parameterize because it will be overriden.