@appthreat/atom-parsetools 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/astgen.js +143 -4
- package/package.json +1 -1
- package/plugins/composer/installed.php +6 -6
package/astgen.js
CHANGED
|
@@ -29,10 +29,11 @@ import { parseSvelteFile, parseSvelteScriptBuffer } from "./svelteAst.js";
|
|
|
29
29
|
// Printed by `astgen --version`. Downstream frontends (e.g. chen's jssrc2cpg)
|
|
30
30
|
// fold this into their parse-cache fingerprint, so it MUST be bumped whenever
|
|
31
31
|
// the emitted AST/type shape changes — otherwise stale cached parses from an
|
|
32
|
-
// older astgen are silently reused. Bumped to 4.
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
32
|
+
// older astgen are silently reused. Bumped to 4.3.0 for Vue directive
|
|
33
|
+
// expression values: `v-html="x"` / `:prop="x"` / `@event="x"` attribute
|
|
34
|
+
// values in `.vue` templates now emit JSX expression containers (real
|
|
35
|
+
// expression ASTs) instead of string literals.
|
|
36
|
+
const ASTGEN_VERSION = "4.3.0";
|
|
36
37
|
|
|
37
38
|
const HELP_TEXT = `Options:
|
|
38
39
|
-i, --src Source directory [default: "."]
|
|
@@ -669,6 +670,119 @@ declare module "svelte/store" {
|
|
|
669
670
|
|
|
670
671
|
const maskNonNewlineChars = (value) => value.replace(/[^\r\n]/g, " ");
|
|
671
672
|
|
|
673
|
+
// Vue directive attributes - `v-html="expr"`, `:prop="expr"`, `@event="expr"`
|
|
674
|
+
// - carry a JavaScript expression in a quoted string. Babel parses that value
|
|
675
|
+
// as a StringLiteral, which severs the data flow from the script binding into
|
|
676
|
+
// the template: `v-html="rawContent"` has no reference to the `rawContent`
|
|
677
|
+
// binding, so no source-to-sink path can ever be found for the most
|
|
678
|
+
// security-relevant Vue shape. Replacing the value's quotes with braces turns
|
|
679
|
+
// it into a JSX expression container - `v-html={rawContent}` - which parses to
|
|
680
|
+
// a real expression AST. The swap is length-preserving (`"x"` -> `{x}`), so
|
|
681
|
+
// every offset in the emitted AST still maps onto the original file.
|
|
682
|
+
//
|
|
683
|
+
// Only values that parse as an expression are converted, and `v-for`/`v-slot`
|
|
684
|
+
// directives are skipped by name before the parse is even attempted:
|
|
685
|
+
// `v-for="item in items"` DOES parse (`in` is a relational operator), but the
|
|
686
|
+
// resulting expression is meaningless for analysis and would synthesise a read
|
|
687
|
+
// of the loop variable `item` that the script never declares - a spurious
|
|
688
|
+
// reference. Those directives keep their string value.
|
|
689
|
+
//
|
|
690
|
+
// Scoped between the root `<template>` block's opening tag and the last
|
|
691
|
+
// `</template>` that lies OUTSIDE any `<script>` block: a script string could
|
|
692
|
+
// legitimately contain the literal text `</template>`, and rewriting inside
|
|
693
|
+
// `<script>` would change script semantics while still parsing - a silent
|
|
694
|
+
// corruption. That still narrows, not eliminates, the risk - a `</template>`
|
|
695
|
+
// inside a template-side attribute value could in principle swallow markup up
|
|
696
|
+
// to a later close - which is why the whole-file candidates remain as
|
|
697
|
+
// fallbacks for any file the converted candidate fails to parse.
|
|
698
|
+
const VUE_DIRECTIVE_ATTR_REGEX =
|
|
699
|
+
/(\sv-[\w.:-]+|\s[:@.][\w.:-]*)=("(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*')/g;
|
|
700
|
+
const VUE_TEMPLATE_OPEN_REGEX = /<template\b[^>]*>/i;
|
|
701
|
+
const VUE_TEMPLATE_CLOSE = "</template>";
|
|
702
|
+
|
|
703
|
+
// `v-for` and `v-slot` (incl. `v-slot:name`) values are not plain expressions.
|
|
704
|
+
const VUE_NON_EXPRESSION_DIRECTIVES = /^v-(for|slot)\b/i;
|
|
705
|
+
|
|
706
|
+
const DIRECTIVE_VALUE_PARSE_OPTIONS = {
|
|
707
|
+
sourceType: "unambiguous",
|
|
708
|
+
allowImportExportEverywhere: true,
|
|
709
|
+
allowAwaitOutsideFunction: true,
|
|
710
|
+
allowReturnOutsideFunction: true,
|
|
711
|
+
allowSuperOutsideMethod: true,
|
|
712
|
+
allowUndeclaredExports: true,
|
|
713
|
+
errorRecovery: false,
|
|
714
|
+
plugins: babelSyntaxPlugins
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
// Ranges of the file covered by `<script ...>...</script>` blocks, so the
|
|
718
|
+
// template-region search can ignore `</template>` text that only appears
|
|
719
|
+
// inside a script string.
|
|
720
|
+
const vueScriptRanges = (code) => {
|
|
721
|
+
const ranges = [];
|
|
722
|
+
let scriptMatch;
|
|
723
|
+
vueScriptTagRegex.lastIndex = 0;
|
|
724
|
+
while ((scriptMatch = vueScriptTagRegex.exec(code)) !== null) {
|
|
725
|
+
ranges.push([scriptMatch.index, scriptMatch.index + scriptMatch[0].length]);
|
|
726
|
+
}
|
|
727
|
+
return ranges;
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const vueDirectiveValueToExpression = (code) => {
|
|
731
|
+
const templateOpen = code.match(VUE_TEMPLATE_OPEN_REGEX);
|
|
732
|
+
if (!templateOpen) {
|
|
733
|
+
return { code, changed: false };
|
|
734
|
+
}
|
|
735
|
+
const templateStart = templateOpen.index + templateOpen[0].length;
|
|
736
|
+
const scriptRanges = vueScriptRanges(code);
|
|
737
|
+
const inScript = (index) =>
|
|
738
|
+
scriptRanges.some(([start, end]) => index >= start && index < end);
|
|
739
|
+
let templateEnd = -1;
|
|
740
|
+
let searchFrom = templateStart;
|
|
741
|
+
let closeIndex;
|
|
742
|
+
while (
|
|
743
|
+
(closeIndex = code.toLowerCase().indexOf(VUE_TEMPLATE_CLOSE, searchFrom)) !==
|
|
744
|
+
-1
|
|
745
|
+
) {
|
|
746
|
+
if (!inScript(closeIndex)) {
|
|
747
|
+
templateEnd = closeIndex;
|
|
748
|
+
}
|
|
749
|
+
searchFrom = closeIndex + 1;
|
|
750
|
+
}
|
|
751
|
+
if (templateEnd < templateStart) {
|
|
752
|
+
return { code, changed: false };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let changed = false;
|
|
756
|
+
const convert = (templateSlice) =>
|
|
757
|
+
templateSlice.replace(
|
|
758
|
+
VUE_DIRECTIVE_ATTR_REGEX,
|
|
759
|
+
(match, namePart, quoted) => {
|
|
760
|
+
if (VUE_NON_EXPRESSION_DIRECTIVES.test(namePart.trim())) {
|
|
761
|
+
return match;
|
|
762
|
+
}
|
|
763
|
+
const inner = quoted.slice(1, -1);
|
|
764
|
+
// An empty value, or a `{{` opening (Vue ignores interpolations in
|
|
765
|
+
// attribute values), is left alone.
|
|
766
|
+
if (!inner.trim() || inner.includes("{{")) {
|
|
767
|
+
return match;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
parse(`(${inner})`, DIRECTIVE_VALUE_PARSE_OPTIONS);
|
|
771
|
+
} catch {
|
|
772
|
+
return match;
|
|
773
|
+
}
|
|
774
|
+
changed = true;
|
|
775
|
+
return `${namePart}={${inner}}`;
|
|
776
|
+
}
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const converted =
|
|
780
|
+
code.slice(0, templateStart) +
|
|
781
|
+
convert(code.slice(templateStart, templateEnd)) +
|
|
782
|
+
code.slice(templateEnd);
|
|
783
|
+
return { code: converted, changed };
|
|
784
|
+
};
|
|
785
|
+
|
|
672
786
|
const cleanVueCodeForParsing = (code, { includeScripts = true } = {}) => {
|
|
673
787
|
let cleanedCode = code
|
|
674
788
|
.replace(vueCommentRegex, (match) => maskNonNewlineChars(match))
|
|
@@ -730,7 +844,32 @@ const buildVueParseCandidates = (code) => {
|
|
|
730
844
|
? `${scriptOnlyCandidate}\n${templateOnlyCandidate}`
|
|
731
845
|
: templateOnlyCandidate;
|
|
732
846
|
|
|
847
|
+
// Directive-expression candidates first: same masking, but `v-html="x"`
|
|
848
|
+
// style values are expression containers, so template expressions keep
|
|
849
|
+
// their references. The plain candidates below remain as fallbacks for the
|
|
850
|
+
// (checked-per-attribute, but defensive) case of a converted file not
|
|
851
|
+
// parsing as a whole.
|
|
852
|
+
const directiveCandidates = (() => {
|
|
853
|
+
const { code: directiveCode, changed } = vueDirectiveValueToExpression(
|
|
854
|
+
code
|
|
855
|
+
);
|
|
856
|
+
if (!changed) {
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
const directiveFull = cleanVueCodeForParsing(directiveCode, {
|
|
860
|
+
includeScripts: true
|
|
861
|
+
});
|
|
862
|
+
const directiveTemplateOnly = cleanVueCodeForParsing(directiveCode, {
|
|
863
|
+
includeScripts: false
|
|
864
|
+
});
|
|
865
|
+
return [
|
|
866
|
+
{ name: "directive-full", code: directiveFull },
|
|
867
|
+
{ name: "directive-template-only", code: directiveTemplateOnly }
|
|
868
|
+
];
|
|
869
|
+
})();
|
|
870
|
+
|
|
733
871
|
const candidates = [
|
|
872
|
+
...directiveCandidates,
|
|
734
873
|
{ name: "full", code: fullCandidate },
|
|
735
874
|
{ name: "combined", code: combinedCandidate },
|
|
736
875
|
{ name: "template-only", code: templateOnlyCandidate },
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<?php return array(
|
|
2
2
|
'root' => array(
|
|
3
3
|
'name' => '__root__',
|
|
4
|
-
'pretty_version' => 'v1.
|
|
5
|
-
'version' => '1.
|
|
6
|
-
'reference' => '
|
|
4
|
+
'pretty_version' => 'v1.7.0',
|
|
5
|
+
'version' => '1.7.0.0',
|
|
6
|
+
'reference' => '26f7c3eeab1661fdf7ee1a5a7b40f0fd034235eb',
|
|
7
7
|
'type' => 'library',
|
|
8
8
|
'install_path' => __DIR__ . '/../../',
|
|
9
9
|
'aliases' => array(),
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
),
|
|
12
12
|
'versions' => array(
|
|
13
13
|
'__root__' => array(
|
|
14
|
-
'pretty_version' => 'v1.
|
|
15
|
-
'version' => '1.
|
|
16
|
-
'reference' => '
|
|
14
|
+
'pretty_version' => 'v1.7.0',
|
|
15
|
+
'version' => '1.7.0.0',
|
|
16
|
+
'reference' => '26f7c3eeab1661fdf7ee1a5a7b40f0fd034235eb',
|
|
17
17
|
'type' => 'library',
|
|
18
18
|
'install_path' => __DIR__ . '/../../',
|
|
19
19
|
'aliases' => array(),
|