Skip to content

Commit 6027082

Browse files
authored
Merge branch 'main' into workflow/coverage/update
2 parents 9163e16 + 0d52541 commit 6027082

File tree

10 files changed

+128
-28
lines changed

10 files changed

+128
-28
lines changed

cpp/ql/src/Architecture/Refactoring Opportunities/ClassesWithManyFields.ql

+1-6
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,7 @@ where
168168
strictcount(string fieldName |
169169
exists(Field f |
170170
f.getDeclaringType() = c and
171-
fieldName = f.getName() and
172-
// IBOutlet's are a way of building GUIs
173-
// automatically out of ObjC properties.
174-
// We don't want to count those for the
175-
// purposes of this query.
176-
not f.getType().getAnAttribute().hasName("iboutlet")
171+
fieldName = f.getName()
177172
)
178173
) and
179174
n > 15 and

javascript/extractor/src/com/semmle/js/extractor/HTMLExtractor.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ private boolean isAngularTemplateAttributeName(String name) {
186186

187187
/** Attribute names that look valid in HTML or in one of the template languages we support, like Vue and Angular. */
188188
private static final Pattern VALID_ATTRIBUTE_NAME =
189-
Pattern.compile("[*:@]?\\[?\\(?[\\w:_\\-.]+\\]?\\)?");
189+
Pattern.compile("[*:@]?\\[?\\(?[\\w:_\\-.]+\\)?\\]?");
190190

191191
/** List of HTML attributes whose value is interpreted as JavaScript. */
192192
private static final Pattern JS_ATTRIBUTE =

javascript/extractor/src/com/semmle/js/extractor/Main.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class Main {
4141
* A version identifier that should be updated every time the extractor changes in such a way that
4242
* it may produce different tuples for the same file under the same {@link ExtractorConfig}.
4343
*/
44-
public static final String EXTRACTOR_VERSION = "2024-10-29";
44+
public static final String EXTRACTOR_VERSION = "2025-01-09";
4545

4646
public static final Pattern NEWLINE = Pattern.compile("\n");
4747

javascript/ql/lib/semmle/javascript/DOM.qll

+28-18
Original file line numberDiff line numberDiff line change
@@ -388,23 +388,33 @@ module DOM {
388388
}
389389
}
390390

391-
/**
392-
* Gets a reference to a DOM event.
393-
*/
394-
private DataFlow::SourceNode domEventSource() {
395-
// e.g. <form onSubmit={e => e.target}/>
396-
exists(JsxAttribute attr | attr.getName().matches("on%") |
397-
result = attr.getValue().flow().getABoundFunctionValue(0).getParameter(0)
398-
)
399-
or
400-
// node.addEventListener("submit", e => e.target)
401-
result = domValueRef().getAMethodCall("addEventListener").getABoundCallbackParameter(1, 0)
402-
or
403-
// node.onSubmit = (e => e.target);
404-
exists(DataFlow::PropWrite write | write = domValueRef().getAPropertyWrite() |
405-
write.getPropertyName().matches("on%") and
406-
result = write.getRhs().getAFunctionValue().getParameter(0)
407-
)
391+
/** A data flow node that is a source of DOM events. */
392+
class DomEventSource extends DataFlow::Node instanceof DomEventSource::Range { }
393+
394+
/** Companion module to the `DomEventSource` class. */
395+
module DomEventSource {
396+
/**
397+
* A data flow node that should be considered a source of DOM events.
398+
*/
399+
abstract class Range extends DataFlow::Node { }
400+
401+
private class DefaultRange extends Range {
402+
DefaultRange() {
403+
// e.g. <form onSubmit={e => e.target}/>
404+
exists(JsxAttribute attr | attr.getName().matches("on%") |
405+
this = attr.getValue().flow().getABoundFunctionValue(0).getParameter(0)
406+
)
407+
or
408+
// node.addEventListener("submit", e => e.target)
409+
this = domValueRef().getAMethodCall("addEventListener").getABoundCallbackParameter(1, 0)
410+
or
411+
// node.onSubmit = (e => e.target);
412+
exists(DataFlow::PropWrite write | write = domValueRef().getAPropertyWrite() |
413+
write.getPropertyName().matches("on%") and
414+
this = write.getRhs().getAFunctionValue().getParameter(0)
415+
)
416+
}
417+
}
408418
}
409419

410420
/** Gets a data flow node that refers directly to a value from the DOM. */
@@ -419,7 +429,7 @@ module DOM {
419429
result = domValueRef().getAMethodCall(["item", "namedItem"])
420430
or
421431
t.startInProp("target") and
422-
result = domEventSource()
432+
result instanceof DomEventSource
423433
or
424434
t.startInProp(DataFlow::PseudoProperties::arrayElement()) and
425435
result = domElementCollection()

javascript/ql/lib/semmle/javascript/frameworks/Angular2.qll

+21
Original file line numberDiff line numberDiff line change
@@ -554,4 +554,25 @@ module Angular2 {
554554
this = API::Node::ofType("@angular/core", "ElementRef").getMember("nativeElement").asSource()
555555
}
556556
}
557+
558+
/**
559+
* A source of DOM events originating from the `$event` variable in an event handler installed in an Angular template.
560+
*/
561+
private class DomEventSources extends DOM::DomEventSource::Range {
562+
DomEventSources() {
563+
exists(HTML::Element elm, string attributeName |
564+
elm = any(ComponentClass cls).getATemplateElement() and
565+
// Ignore instantiations of known element (mainly focus on native DOM elements)
566+
not elm = any(ComponentClass cls).getATemplateInstantiation() and
567+
not elm.getName().matches("ng-%") and
568+
this =
569+
elm.getAttributeByName(attributeName)
570+
.getCodeInAttribute()
571+
.(TemplateTopLevel)
572+
.getAVariableUse("$event") and
573+
attributeName.matches("(%)") and // event handler attribute
574+
not attributeName.matches("(ng%)") // exclude NG events which aren't necessarily DOM events
575+
)
576+
}
577+
}
557578
}

javascript/ql/lib/semmle/javascript/security/dataflow/XssThroughDomCustomizations.qll

+21
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ module XssThroughDom {
232232
)
233233
}
234234
}
235+
236+
/**
237+
* An object containing input values from an Angular form, accessed through an `NgForm` object.
238+
*/
239+
class AngularFormSource extends Source {
240+
AngularFormSource() {
241+
this = API::Node::ofType("@angular/forms", "NgForm").getMember("value").asSource()
242+
}
243+
}
235244
}
236245

237246
/**
@@ -261,4 +270,16 @@ module XssThroughDom {
261270
this = getSelectionCall(DataFlow::TypeTracker::end()).getAMethodCall("toString")
262271
}
263272
}
273+
274+
/**
275+
* A source of DOM input originating from an Angular two-way data binding.
276+
*/
277+
private class AngularNgModelSource extends Source {
278+
AngularNgModelSource() {
279+
exists(Angular2::ComponentClass component, string fieldName |
280+
fieldName = component.getATemplateElement().getAttributeByName("[(ngModel)]").getValue() and
281+
this = component.getFieldInputNode(fieldName)
282+
)
283+
}
284+
}
264285
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* The `js/xss-through-dom` query now recognises sources of DOM input originating from Angular templates.

javascript/ql/test/query-tests/Security/CWE-079/XssThroughDom/XssThroughDom.expected

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
edges
2+
| angular.ts:12:5:12:23 | field: string = ""; | angular.ts:33:24:33:33 | this.field | provenance | |
3+
| angular.ts:29:24:29:33 | form.value | angular.ts:29:24:29:37 | form.value.foo | provenance | |
24
| forms.js:8:23:8:28 | values | forms.js:9:31:9:36 | values | provenance | |
35
| forms.js:9:31:9:36 | values | forms.js:9:31:9:40 | values.foo | provenance | |
46
| forms.js:11:24:11:29 | values | forms.js:12:31:12:36 | values | provenance | |
@@ -42,6 +44,12 @@ edges
4244
| xss-through-dom.js:154:25:154:27 | msg | xss-through-dom.js:155:27:155:29 | msg | provenance | |
4345
| xss-through-dom.js:159:34:159:52 | $("textarea").val() | xss-through-dom.js:154:25:154:27 | msg | provenance | |
4446
nodes
47+
| angular.ts:12:5:12:23 | field: string = ""; | semmle.label | field: string = ""; |
48+
| angular.ts:16:24:16:41 | event.target.value | semmle.label | event.target.value |
49+
| angular.ts:20:24:20:35 | target.value | semmle.label | target.value |
50+
| angular.ts:29:24:29:33 | form.value | semmle.label | form.value |
51+
| angular.ts:29:24:29:37 | form.value.foo | semmle.label | form.value.foo |
52+
| angular.ts:33:24:33:33 | this.field | semmle.label | this.field |
4553
| forms.js:8:23:8:28 | values | semmle.label | values |
4654
| forms.js:9:31:9:36 | values | semmle.label | values |
4755
| forms.js:9:31:9:40 | values.foo | semmle.label | values.foo |
@@ -124,6 +132,10 @@ nodes
124132
| xss-through-dom.js:159:34:159:52 | $("textarea").val() | semmle.label | $("textarea").val() |
125133
subpaths
126134
#select
135+
| angular.ts:16:24:16:41 | event.target.value | angular.ts:16:24:16:41 | event.target.value | angular.ts:16:24:16:41 | event.target.value | $@ is reinterpreted as HTML without escaping meta-characters. | angular.ts:16:24:16:41 | event.target.value | DOM text |
136+
| angular.ts:20:24:20:35 | target.value | angular.ts:20:24:20:35 | target.value | angular.ts:20:24:20:35 | target.value | $@ is reinterpreted as HTML without escaping meta-characters. | angular.ts:20:24:20:35 | target.value | DOM text |
137+
| angular.ts:29:24:29:37 | form.value.foo | angular.ts:29:24:29:33 | form.value | angular.ts:29:24:29:37 | form.value.foo | $@ is reinterpreted as HTML without escaping meta-characters. | angular.ts:29:24:29:33 | form.value | DOM text |
138+
| angular.ts:33:24:33:33 | this.field | angular.ts:12:5:12:23 | field: string = ""; | angular.ts:33:24:33:33 | this.field | $@ is reinterpreted as HTML without escaping meta-characters. | angular.ts:12:5:12:23 | field: string = ""; | DOM text |
127139
| forms.js:9:31:9:40 | values.foo | forms.js:8:23:8:28 | values | forms.js:9:31:9:40 | values.foo | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:8:23:8:28 | values | DOM text |
128140
| forms.js:12:31:12:40 | values.bar | forms.js:11:24:11:29 | values | forms.js:12:31:12:40 | values.bar | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:11:24:11:29 | values | DOM text |
129141
| forms.js:25:23:25:34 | values.email | forms.js:24:15:24:20 | values | forms.js:25:23:25:34 | values.email | $@ is reinterpreted as HTML without escaping meta-characters. | forms.js:24:15:24:20 | values | DOM text |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Component } from "@angular/core";
2+
import { NgForm } from "@angular/forms";
3+
4+
@Component({
5+
template: `
6+
<input type="text" (input)="setInput1($event)"></input>
7+
<input type="text" (input)="setInput2($event.target)"></input>
8+
<input type="text" [(ngModel)]="field"></input>
9+
`
10+
})
11+
export class Foo {
12+
field: string = "";
13+
safeField: string = "";
14+
15+
setInput1(event) {
16+
document.write(event.target.value); // NOT OK
17+
}
18+
19+
setInput2(target) {
20+
document.write(target.value); // NOT OK
21+
}
22+
23+
setOtherInput(e) {
24+
document.write(e.target.value); // OK
25+
document.write(e.value); // OK
26+
}
27+
28+
blah(form: NgForm) {
29+
document.write(form.value.foo); // NOT OK
30+
}
31+
32+
useField() {
33+
document.write(this.field); // NOT OK
34+
document.write(this.safeField); // OK
35+
}
36+
}

shared/dataflow/codeql/dataflow/internal/DataFlowImpl.qll

+3-2
Original file line numberDiff line numberDiff line change
@@ -2154,8 +2154,7 @@ module MakeImpl<LocationSig Location, InputSig<Location> Lang> {
21542154
pragma[nomagic]
21552155
private predicate storeStepFwd(NodeEx node1, Ap ap1, Content c, NodeEx node2, Ap ap2) {
21562156
fwdFlowStore(node1, _, ap1, _, c, _, _, node2, _, _, _) and
2157-
ap2 = apCons(c, ap1) and
2158-
readStepFwd(_, ap2, c, _, _)
2157+
readStepFwd(_, ap2, c, _, ap1)
21592158
}
21602159

21612160
pragma[nomagic]
@@ -4393,6 +4392,7 @@ module MakeImpl<LocationSig Location, InputSig<Location> Lang> {
43934392
Typ getTyp(DataFlowType t) { result = t }
43944393

43954394
bindingset[c, tail]
4395+
pragma[inline_late]
43964396
Ap apCons(Content c, Ap tail) { result.isCons(c, tail) }
43974397

43984398
class ApHeadContent = Content;
@@ -4462,6 +4462,7 @@ module MakeImpl<LocationSig Location, InputSig<Location> Lang> {
44624462
abstract Content getHead();
44634463

44644464
/** Holds if this is a representation of `head` followed by `tail`. */
4465+
pragma[nomagic]
44654466
abstract predicate isCons(Content head, AccessPath tail);
44664467

44674468
/** Gets the front of this access path. */

0 commit comments

Comments
 (0)