forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfingerprintApis.mjs
More file actions
189 lines (172 loc) · 5.36 KB
/
fingerprintApis.mjs
File metadata and controls
189 lines (172 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import _ from 'lodash';
const weightsUrl = `https://raw.githubusercontent.com/duckduckgo/tracker-radar/refs/heads/main/build-data/generated/api_fingerprint_weights.json`;
import fs from 'fs/promises';
import path from 'path';
const MIN_WEIGHT = 15;
const QUERY_DIR = path.join(import.meta.dirname, '.github/codeql/queries');
const QUERY_FILE_PREFIX = 'autogen_';
async function fetchWeights() {
const weights = await fetch(weightsUrl).then(res => res.json());
return Object.fromEntries(
Object.entries(weights).filter(([api, weight]) => weight >= MIN_WEIGHT)
);
}
const QUERY_FILE_TPL = _.template(`/**
* @id prebid/<%= id %>
* @name <%= name %>
* @kind problem
* @problem.severity warning
* @description <%= description %>
*/
// this file is autogenerated, see fingerprintApis.mjs
<%= query %>
`);
function message(weight, api) {
return `"${api} is an indicator of fingerprinting, weighed ${weight} in ${weightsUrl}"`
}
function windowProp(weight, api) {
return [
`window_${api}`,
QUERY_FILE_TPL({
id: `window-${api}`.toLowerCase(),
name: `Access to window.${api}`,
description: `Finds uses of window.${api}`,
query: `import prebid
from SourceNode api
where
api = windowPropertyRead("${api}")
select api, ${message(weight, api)}`
})
]
}
function globalProp(prop) {
return function (weight, api) {
return [
`${prop}_${api}`,
QUERY_FILE_TPL({
id: `${prop}-${api}`.toLowerCase(),
name: `Access to ${prop}.${api}`,
description: `Finds uses of ${prop}.${api}`,
query: `import prebid
from SourceNode prop, SourceNode api
where
prop = windowPropertyRead("${prop}") and
api = prop.getAPropertyRead("${api}")
select api, ${message(weight, api)}`
})
]
}
}
function globalConstructor(weight, ctr) {
return [
`${ctr}`,
QUERY_FILE_TPL({
id: `${ctr}`.toLowerCase(),
name: `Use of ${ctr}`,
description: `Finds uses of ${ctr}`,
query: `import prebid
from SourceNode api
where
api = instantiationOf("${ctr}")
select api, ${message(weight, ctr)}`
})
]
}
function globalConstructorProperty(weight, ctr, api) {
return [
`${ctr}_${api}`,
QUERY_FILE_TPL({
id: `${ctr}-${api}`.toLowerCase(),
name: `Access to ${ctr}.${api}`,
description: `Finds uses of ${ctr}.${api}`,
query: `import prebid
from SourceNode inst, SourceNode api
where
inst = instantiationOf("${ctr}") and
api = inst.getAPropertyRead("${api}")
select api, ${message(weight, api)}`
})
]
}
function simplePropertyMatch(weight, target, prop) {
return [
`${target}_${prop}`,
QUERY_FILE_TPL({
id: `${target}-${prop}`.toLowerCase(),
name: `Potential access to ${target}.${prop}`,
description: `Finds uses of ${prop}`,
query: `import prebid
from PropRef api
where
api.getPropertyName() = "${prop}"
select api, ${message(weight, prop)}`
})
]
}
function glContextMatcher(contextType) {
return function(weight, api) {
return [
`${contextType}_RenderingContext_${api}`,
QUERY_FILE_TPL({
id: `${contextType}-${api}`.toLowerCase(),
name: `Access to ${contextType} rendering context ${api}`,
description: `Finds uses of ${contextType} RenderingContext.${api}`,
query: `import prebid
from InvokeNode invocation, SourceNode api
where
invocation.getCalleeName() = "getContext" and
invocation.getArgument(0).mayHaveStringValue("${contextType}") and
api = invocation.getAPropertyRead("${api}")
select api, ${message(weight, api)}`
})
]
}
}
const API_MATCHERS = [
[/^([^.]+)\.prototype.constructor$/, globalConstructor],
[/^Screen\.prototype\.(.*)$/, globalProp('screen')],
[/^Notification\.([^.]+)$/, globalProp('Notification')],
[/^window\.(.*)$/, windowProp],
[/^Navigator.prototype\.(.*)$/, globalProp('navigator')],
[/^(Date|Gyroscope)\.prototype\.(.*)$/, globalConstructorProperty],
[/^(DeviceMotionEvent)\.prototype\.(.*)$/, simplePropertyMatch],
[/^WebGLRenderingContext\.prototype\.(.*)$/, glContextMatcher('webgl')],
[/^WebGL2RenderingContext\.prototype\.(.*)$/, glContextMatcher('webgl2')],
[/^CanvasRenderingContext2D\.prototype\.(.*)$/, glContextMatcher('2d')],
];
async function generateQueries() {
const weights = await fetchWeights();
const queries = {};
Object.entries(weights).filter(([identifier, weight]) => {
for (const [matcher, queryGen] of API_MATCHERS) {
const match = matcher.exec(identifier);
if (match) {
const [name, query] = queryGen(weight, ...match.slice(1));
queries[name] = query;
delete weights[identifier];
break;
}
}
})
const unmatched = Object.keys(weights);
if (Object.keys(weights).length > 0) {
console.warn(`The following APIs are weighed more than ${MIN_WEIGHT}, but no query was generated for them:`, JSON.stringify(weights, null, 2))
}
return queries;
}
async function clearFiles() {
for (const file of await fs.readdir(QUERY_DIR)) {
if (file.startsWith(QUERY_FILE_PREFIX)) {
await fs.rm(path.join(QUERY_DIR, file))
}
}
}
async function generateQueryFiles() {
for (const [name, query] of Object.entries(await generateQueries())) {
await fs.writeFile(path.join(QUERY_DIR, `autogen_${name}.ql`), query);
}
}
export async function updateQueries() {
await clearFiles();
await generateQueryFiles();
}