-
Notifications
You must be signed in to change notification settings - Fork 379
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
Ajv loading requires unsafe-eval
policy for script-src
Content-Security-Policy
#1498
Comments
I like the look of ajv-pack, this is also related to a potential performance enhancement discussed here. Pre-complilation is great because avoids a penalty for doing this at runtime which is non-trivial for large schemas. Although ajv-pack does not support recursive references which is a feature the eclipsesource team are currently looking to support. #1476 I wonder if its possible to use ajv-pack already by passing an object to init which implements the methods of ajv that JSON Forms uses but uses ajv-pack instead and example of use Something like
|
An inital test suggests more compatibility work would be required to use ajv-pack. I used the validate function created by dispatching init action
First big difference is that I only see 1st error, not array of errors from the function created by ajv-pack. Second difference which breaks the displaying of validation errors is that dataPath is missing Ajv-pack validate
AJV default validation (I removed schema and data properties)
|
Okay first issue is fixed by compling schema with --all-errors flag
|
Thanks @Lily418 for the great responses. I think it makes sense to support ajv-pack, either directly or at least as some sort of configuration option in the future. We should also check whether we can reduce the bundle size for people who want to use Ajv this way. I checked the code for the usages of Ajv and found:
These three places need to be modified for a proper support of ajv-pack. Items 1 & 2 could be configured using @Lily418's suggestion with a custom Ajv "mock" (assuming a We would welcome a PR providing a configuration option for ajv-pack. |
My issue was caused by not compiling schema with correct flags. I added
Here's a little script based off the one provided by ajv-pack to compile schema with default Json Forms Ajv options https://gist.github.com/Lily418/0fb96b6dd4ae87885f0c788b070383ac |
Thanks again @Lily418 for your great input. @clayroach when using this compile script and creating your own "Ajv mock" as outlined above, you should be able to use JSON Forms (except for the Rule support) without hitting an error caused by the configured policies. |
This issue will be closed as it has been open for a long time with no activity. Feel free to reopen this issue if needed. |
@sdirix This is still a real issue. If you could re-open that would be amazing. I'm using JSON forms for a project right now and just ran into this. I see ways it can be resolved and I think I may be able to create a PR at some point in the next few weeks or so. I found that using the method suggested by @Lily418 above to generate the Ideally, it would be amazing if we could inject the EDIT: After digging in a bit further, it looks like the main issue with "id":{
"type":"string",
"format":"uri" // This one
},
"$schema":{
"type":"string",
"format":"uri" // and this one
}, If JSON Forms could function with the default draft-4 schema, and add it with |
Hi @DBosley, thanks for your message. I agree we should keep this issue open. I'm absolutely fine with changing Regarding the API support: Maybe it would be easiest to just provide a wrapper util for packed modules which mocks / delegates all Ajv functions which JSON Forms uses. The usage would be straightforward and we don't need to distinguish between "normal" Ajv and "packed" Ajv in the core module, reducing complexity. Do you see any problems with that or is there something we would miss out upon with this approach? |
I think passing the ajv instance into If all usages of AJV could use the one provided via config, there would at least be a workaround that would allow users to get around unsafe-eval, even if it's a little hacky. Some things to note: import { validate } from "./packedValidate";
const schema = validate.schema;
...
validate(formData); // this is functionally the same as Ajv.compile(schema)(formData) I like your idea for the mock/delegate approach. I think it could get the job done. Is this something you want to take the initiative on, or would it help if I took a stab at it in a PR or two? I think 2 PRs make sense for this:
I can most likely get the first PR done pretty quickly. (I may need a little help if this is something that affects tests or needs a new test, but otherwise I'm fairly familiar with the code at this point. I've had to develop a full set of renderers and cells for my project). The 2nd PR should be simple as well, but I'd love some guidance on how you'd like it done, any tests it may need, and documentation improvements that would be required. This does seem like it may be related to #1557 |
This topic is currently not on our priority list so we wouldn't tackle it in the near future ourselves (except when a client requests it via the support). However we'll definitively take a look at a contribution from the community (i.e. you 😉 ). Of course if there are any questions we'll gladly help.
|
good news! I don't know how in-depth I will be able to go for making the ajv-packed improvements myself. I'm happy to assist, but I need to shift back to my own project once I have a way to move forward (hopefully this first PR will do the trick). I'll document anything I need to do to get this working that may be useful for building the ajv-packed injector. |
That's fine of course ;) Yes it would be very helpful to know what you had to do to get it working. |
I was able to use the latest beta version with my precompiled AJV schema validator and I didn't get an unsafe-eval error with CSP enabled! Here's how I got it to work:
const Ajv = require('ajv');
const pack = require('ajv-pack');
const fs = require('fs');
const path = require('path');
const process = require('process');
const ajv = new Ajv({
sourceCode: true,
schemaId: 'auto',
allErrors: true,
jsonPointers: true,
errorDataPath: 'property',
verbose: true
});
const schemaPath = path.join(__dirname, './schemas');
const generatedFileText =
'// THIS FILE IS GENERATED. DO NOT EDIT\n\n';
let indexFile = '';
fs.readdir(schemaPath, (err, files) => {
if (err) {
console.error('Could not list the directory.', err);
process.exit(1);
}
files.forEach((file) => {
const filePath = path.join(schemaPath, file);
const schema = require(filePath);
const validate = ajv.compile(schema);
const moduleCode = pack(ajv, validate);
const validatorName = file.split('.')[0];
const outFile = validatorName + '.js';
fs.writeFileSync(
path.join(__dirname, './validators', outFile),
generatedFileText + moduleCode
);
indexFile += `export { default as ${validatorName} } from './validators/${validatorName}'\n`;
});
fs.writeFileSync(path.join(__dirname, './index.js'), generatedFileText + indexFile);
});
import Ajv from 'ajv';
import { MySchema as validate } from './path/to/generated/index';
const ajv = new Ajv({
sourceCode: true,
schemaId: 'auto',
allErrors: true,
jsonPointers: true,
errorDataPath: 'property',
verbose: true
});
ajv.compile = () => validate;
|
@DBosley Thanks for the extensive write-up, this is very helpful! Did you also manage to get the schema-based rule support working? |
I don't actually use that in my implementation. I don't see that being a problem, though. The unsafe-eval issues stem from compiling JSON Schemas and meta schemas with AJV. As far as I can tell, nothing in the UI Schema is compiled. I do use some UI Schema settings to organize layout, so I know the reference selectors still work fine. I may be using some of the ui schema rules soon, but I'm not sure yet. I'll let you know if I have time to test that out |
Small update: I got this working with multiple schemas that have const pack = require('ajv-pack');
const { join, dirname } = require('path');
const fs = require('fs');
const { mkdirSync, writeFileSync } = fs;
// id is /path/to/my/schema.json
// I also use this as the $id string in the schema
const validate = ajv.getSchema(id);
const moduleCode = pack(ajv, validate);
const validatorName = id.split('.')[0];
const outFile = join(__dirname, '../packed', validatorName + '.js');
mkdirSync(dirname(outFile), { recursive: true });
writeFileSync(outFile, generatedFileText + moduleCode, {}); There's an issue with just replacing _ajv.compile = (schema) => {
if (schema['$id']) {
const validators = [
rootValidate,
ref1Validate,
ref2Validate
];
for (const validator of validators) {
if (validator.schema['$id'] === schema['$id']) {
return validator;
}
}
}
return rootValidate;
}; Also, you need to create resolvers for each schema to pass into the JSON Forms component: const resolve = {
ref1: {
order: 1,
canRead: (file) => {
return file.url.indexOf('name-of-my-ref1-file') !== -1;
},
read: () => {
return JSON.stringify(ref1Validate.schema);
}
},
ref2: {
order: 2,
canRead: (file) => {
return file.url.indexOf('name-of-my-ref2-file') !== -1;
},
read: () => {
return JSON.stringify(ref2Validate.schema);
}
}
}; Then this is just handed in like it is in the documentation here: https://jsonforms.io/docs/ref-resolving |
ajv-pack is now deprecated and its functionality has been integrated/moved to ajv itself and ajv-cli. The output of ajv-cli seems to differ slightly from ajv-pack (at least I wasn't able to directly use @DBosley's workaround, #1498 (comment)) Are there any plans to move forward with this issue? Here's some output produced by ajv-cli on an example schema. Click to expand'use strict' module.exports = validate20 module.exports.default = validate20 const schema22 = { type: 'object', properties: { name: { type: 'string', minLength: 3, description: 'Please enter your name', }, vegetarian: { type: 'boolean' }, birthDate: { type: 'string', format: 'date' }, nationality: { type: 'string', enum: ['DE', 'IT', 'JP', 'US', 'RU', 'Other'], }, personalData: { type: 'object', properties: { age: { type: 'integer', description: 'Please enter your age.' }, height: { type: 'number' }, drivingSkill: { type: 'number', maximum: 10, minimum: 1, default: 7 }, }, required: ['age', 'height'], }, occupation: { type: 'string' }, postalCode: { type: 'string', maxLength: 5 }, }, required: ['occupation', 'nationality'], } const func8 = require('ajv/dist/runtime/ucs2length').default const func0 = require('ajv/dist/runtime/equal').default const formats0 = require('ajv-formats/dist/formats').fullFormats.date function validate20( data, { instancePath = '', parentData, parentDataProperty, rootData = data } = {} ) { let vErrors = null let errors = 0 if (errors === 0) { if (data && typeof data == 'object' && !Array.isArray(data)) { let missing0 if ( (data.occupation === undefined && (missing0 = 'occupation')) || (data.nationality === undefined && (missing0 = 'nationality')) ) { validate20.errors = [ { instancePath, schemaPath: '#/required', keyword: 'required', params: { missingProperty: missing0 }, message: "must have required property '" + missing0 + "'", }, ] return false } else { if (data.name !== undefined) { let data0 = data.name const _errs1 = errors if (errors === _errs1) { if (typeof data0 === 'string') { if (func8(data0) < 3) { validate20.errors = [ { instancePath: instancePath + '/name', schemaPath: '#/properties/name/minLength', keyword: 'minLength', params: { limit: 3 }, message: 'must NOT have fewer than 3 characters', }, ] return false } } else { validate20.errors = [ { instancePath: instancePath + '/name', schemaPath: '#/properties/name/type', keyword: 'type', params: { type: 'string' }, message: 'must be string', }, ] return false } } var valid0 = _errs1 === errors } else { var valid0 = true } if (valid0) { if (data.vegetarian !== undefined) { const _errs3 = errors if (typeof data.vegetarian !== 'boolean') { validate20.errors = [ { instancePath: instancePath + '/vegetarian', schemaPath: '#/properties/vegetarian/type', keyword: 'type', params: { type: 'boolean' }, message: 'must be boolean', }, ] return false } var valid0 = _errs3 === errors } else { var valid0 = true } if (valid0) { if (data.birthDate !== undefined) { let data2 = data.birthDate const _errs5 = errors if (errors === _errs5) { if (errors === _errs5) { if (typeof data2 === 'string') { if (!formats0.validate(data2)) { validate20.errors = [ { instancePath: instancePath + '/birthDate', schemaPath: '#/properties/birthDate/format', keyword: 'format', params: { format: 'date' }, message: 'must match format "' + 'date' + '"', }, ] return false } } else { validate20.errors = [ { instancePath: instancePath + '/birthDate', schemaPath: '#/properties/birthDate/type', keyword: 'type', params: { type: 'string' }, message: 'must be string', }, ] return false } } } var valid0 = _errs5 === errors } else { var valid0 = true } if (valid0) { if (data.nationality !== undefined) { let data3 = data.nationality const _errs7 = errors if (typeof data3 !== 'string') { validate20.errors = [ { instancePath: instancePath + '/nationality', schemaPath: '#/properties/nationality/type', keyword: 'type', params: { type: 'string' }, message: 'must be string', }, ] return false } if ( !( data3 === 'DE' || data3 === 'IT' || data3 === 'JP' || data3 === 'US' || data3 === 'RU' || data3 === 'Other' ) ) { validate20.errors = [ { instancePath: instancePath + '/nationality', schemaPath: '#/properties/nationality/enum', keyword: 'enum', params: { allowedValues: schema22.properties.nationality.enum, }, message: 'must be equal to one of the allowed values', }, ] return false } var valid0 = _errs7 === errors } else { var valid0 = true } if (valid0) { if (data.personalData !== undefined) { let data4 = data.personalData const _errs9 = errors if (errors === _errs9) { if ( data4 && typeof data4 == 'object' && !Array.isArray(data4) ) { let missing1 if ( (data4.age === undefined && (missing1 = 'age')) || (data4.height === undefined && (missing1 = 'height')) ) { validate20.errors = [ { instancePath: instancePath + '/personalData', schemaPath: '#/properties/personalData/required', keyword: 'required', params: { missingProperty: missing1 }, message: "must have required property '" + missing1 + "'", }, ] return false } else { if (data4.age !== undefined) { let data5 = data4.age const _errs11 = errors if ( !( typeof data5 == 'number' && !(data5 % 1) && !isNaN(data5) && isFinite(data5) ) ) { validate20.errors = [ { instancePath: instancePath + '/personalData/age', schemaPath: '#/properties/personalData/properties/age/type', keyword: 'type', params: { type: 'integer' }, message: 'must be integer', }, ] return false } var valid1 = _errs11 === errors } else { var valid1 = true } if (valid1) { if (data4.height !== undefined) { let data6 = data4.height const _errs13 = errors if ( !(typeof data6 == 'number' && isFinite(data6)) ) { validate20.errors = [ { instancePath: instancePath + '/personalData/height', schemaPath: '#/properties/personalData/properties/height/type', keyword: 'type', params: { type: 'number' }, message: 'must be number', }, ] return false } var valid1 = _errs13 === errors } else { var valid1 = true } if (valid1) { if (data4.drivingSkill !== undefined) { let data7 = data4.drivingSkill const _errs15 = errors if (errors === _errs15) { if ( typeof data7 == 'number' && isFinite(data7) ) { if (data7 > 10 || isNaN(data7)) { validate20.errors = [ { instancePath: instancePath + '/personalData/drivingSkill', schemaPath: '#/properties/personalData/properties/drivingSkill/maximum', keyword: 'maximum', params: { comparison: '<=', limit: 10 }, message: 'must be <= 10', }, ] return false } else { if (data7 < 1 || isNaN(data7)) { validate20.errors = [ { instancePath: instancePath + '/personalData/drivingSkill', schemaPath: '#/properties/personalData/properties/drivingSkill/minimum', keyword: 'minimum', params: { comparison: '>=', limit: 1, }, message: 'must be >= 1', }, ] return false } } } else { validate20.errors = [ { instancePath: instancePath + '/personalData/drivingSkill', schemaPath: '#/properties/personalData/properties/drivingSkill/type', keyword: 'type', params: { type: 'number' }, message: 'must be number', }, ] return false } } var valid1 = _errs15 === errors } else { var valid1 = true } } } } } else { validate20.errors = [ { instancePath: instancePath + '/personalData', schemaPath: '#/properties/personalData/type', keyword: 'type', params: { type: 'object' }, message: 'must be object', }, ] return false } } var valid0 = _errs9 === errors } else { var valid0 = true } if (valid0) { if (data.occupation !== undefined) { const _errs17 = errors if (typeof data.occupation !== 'string') { validate20.errors = [ { instancePath: instancePath + '/occupation', schemaPath: '#/properties/occupation/type', keyword: 'type', params: { type: 'string' }, message: 'must be string', }, ] return false } var valid0 = _errs17 === errors } else { var valid0 = true } if (valid0) { if (data.postalCode !== undefined) { let data9 = data.postalCode const _errs19 = errors if (errors === _errs19) { if (typeof data9 === 'string') { if (func8(data9) > 5) { validate20.errors = [ { instancePath: instancePath + '/postalCode', schemaPath: '#/properties/postalCode/maxLength', keyword: 'maxLength', params: { limit: 5 }, message: 'must NOT have more than 5 characters', }, ] return false } } else { validate20.errors = [ { instancePath: instancePath + '/postalCode', schemaPath: '#/properties/postalCode/type', keyword: 'type', params: { type: 'string' }, message: 'must be string', }, ] return false } } var valid0 = _errs19 === errors } else { var valid0 = true } } } } } } } } } else { validate20.errors = [ { instancePath, schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object', }, ] return false } } validate20.errors = vErrors return errors === 0 } |
Could someone explain why rules does not working with standalone(CLI) compiled JSON schema ? Or maybe someone have some workaround now? |
I haven't been able to get @DBosley's approach to work in my project using Ajv standalone validators. The issue for me is that the compiled validator code contains I think the compiled validators should be dependency-free and contain all required code bundled in, or at least there should be an option to bundle everything in. I came up with a hacky workaround: find-and-replace on the compiled code to replace const replacements = [
{
search: /require\("ajv\/dist\/runtime\/ucs2length"\)\.default/g,
replace: 'function(c){let t=c.length,f=0,n=0,d;for(;n<t;)f++,(d=c.charCodeAt(n++))>=55296&&d<=56319&&n<t&&(64512&(d=c.charCodeAt(n)))==56320&&n++;return f}',
},
// ...
];
for (const { search, replace } of replacements) {
validatorCode = validatorCode.replace(search, replace);
}
if (validatorCode.indexOf('require(') !== -1) {
throw new Error('Unreplaced require() statement in compiled validator code');
} With this, the compiled validators run in the browser. I am still working on jerry-rigging the A lot of work currently goes into using this library and Ajv within the context of a secure CSP. |
I was able to get it working with the hack in my previous comment and: export default function Form({
data,
enabled,
onChange,
schema,
validator,
uischema,
validationMode,
}) {
const ajvRef = useRef(createAjv());
ajvRef.current.compile = () => validator;
return (
<JsonForms
ajv={ajvRef.current}
cells={cells}
data={data}
readonly={!enabled}
onChange={onChange}
renderers={renderers}
schema={schema}
uischema={uischema}
validationMode={validationMode}
/>
);
} |
It uses a different API.
|
If we implement a similar "mock" for
|
I have an idea after reading the source. <JsonForms
ajv={undefined}
cells={cells}
data={data}
readonly={!enabled}
onChange={onChange}
renderers={renderers}
schema={schema}
uischema={uischema}
validationMode={"NoValidation"}
additionalErrors={(data) => validator(data)} // validator is compiled externally.
/> You can see this working at https://jsonforms.io/docs/validation#external-validation-errors
|
Nothing here gets my rules working. Has anyone else had success with that? |
Hi @cbprice, You can use the same workaround as a previous poster, but make it a bit smarter: Instead of blindly returning the |
Hey @sdirix, first of all, thank you for working on this! I think this is a very valuable library, especially due to it not being tied to a single frontend framework's ecosystem. IMHO it'd be even more flexible (and more future-proof as restrictive CSP settings become more common) if validation was not tied to Ajv. Now, (unless I'm missing something here) the outlined workarounds solve the issue for mostly static JSON schemas. However, when introducing more dynamic schemas (think building a form builder with dynamic schemas etc.) or when layering schemas, things become difficult. It's a bit unfortunate you'd have to either expose your users to XSS risks by going with I'm aware of #1557 but I think that's more about making Ajv updates more flexible. Any chance we're going to see support for something like hyperjump/json-schema or similar packages not requiring
What are your thoughts on this? |
@sdirix - how we can avoid same error while evaluating rules to control layout rendering e.g show/hide or enabled/disabled element dynamically based on payload value? we have overcome unsafe-eval issue to validate the payload againts schema by writing our custom validator but how we can handle the same issue with rules? We could not go with complied schema approach as we more than 100 layout schema and json schema which we are fetching dynamically and rendering it on our UI, those schemas are frequently changed using dashboard so in this case how we can resolve this issue.? |
Is your feature request related to a problem? Please describe.
For secure environments, the content security policy requires the
unsafe-eval
policy directive for thescript-src
policy. This requirement is primarily due to the use of Ajv's use of theFunction
constructor per ajv-validator/ajv#406Describe the solution you'd like
The best resolution to this is to enable pre-compilation of the JSON schemas so that they don't have to be compiled at runtime. This can be accomplished using
ajv-pack
https://github.com/epoberezkin/ajv-packDescribe alternatives you've considered
Alternatives or workarounds is to lazy load the Ajv validator so that it only loads when JsonForms components are rendered. This would allow compiling a version that disables jsonforms for environment with strict CSP's.
Describe for which setup you like to have the improvement
Framework: Material-UI (or any of the others, this should apply)
Additional context
See screen shots below for some examples of this error
The text was updated successfully, but these errors were encountered: