Skip to content

Commit 600ea7b

Browse files
authored
fix: event parsing panic (solana-foundation#3657)
* add test * stricter regex * less confusing log * even stricter regex * add typescript fix and test * fix * prettier
1 parent fa4f48b commit 600ea7b

3 files changed

Lines changed: 122 additions & 30 deletions

File tree

client/src/lib.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ pub fn handle_system_log(this_program_str: &str, log: &str) -> (Option<String>,
423423
} else if log.contains("invoke") && !log.ends_with("[1]") {
424424
(Some("cpi".to_string()), false) // Any string will do.
425425
} else {
426-
let re = Regex::new(r"^Program (.*) success*$").unwrap();
426+
let re = Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) success$").unwrap();
427427
if re.is_match(log) {
428428
(None, true)
429429
} else {
@@ -441,7 +441,7 @@ impl Execution {
441441
let l = &logs[0];
442442
*logs = &logs[1..];
443443

444-
let re = Regex::new(r"^Program (.*) invoke.*$").unwrap();
444+
let re = Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[[\d]+\]$").unwrap();
445445
let c = re
446446
.captures(l)
447447
.ok_or_else(|| ClientError::LogParseError(l.to_string()))?;
@@ -679,7 +679,7 @@ fn parse_logs_response<T: anchor_lang::Event + anchor_lang::AnchorDeserialize>(
679679
if let Ok(mut execution) = Execution::new(&mut logs) {
680680
// Create a new peekable iterator so that we can peek at the next log whilst iterating
681681
let mut logs_iter = logs.iter().peekable();
682-
let regex = Regex::new(r"^Program (.*) invoke.*$").unwrap();
682+
let regex = Regex::new(r"^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[[\d]+\]$").unwrap();
683683

684684
while let Some(l) = logs_iter.next() {
685685
// Parse the log.
@@ -868,4 +868,37 @@ mod tests {
868868

869869
Ok(())
870870
}
871+
872+
#[test]
873+
fn test_parse_logs_response_fake_pop() -> Result<()> {
874+
let logs = [
875+
"Program fake111111111111111111111111111111111111112 invoke [1]",
876+
"Program log: i logged success",
877+
"Program log: i logged success",
878+
"Program fake111111111111111111111111111111111111112 consumed 1411 of 200000 compute units",
879+
"Program fake111111111111111111111111111111111111112 success"
880+
];
881+
882+
// Converting to Vec<String> as expected in `RpcLogsResponse`
883+
let logs: Vec<String> = logs.iter().map(|&l| l.to_string()).collect();
884+
885+
let program_id_str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
886+
887+
// No events returned here. Just ensuring that the function doesn't panic
888+
// due an incorrectly emptied stack.
889+
parse_logs_response::<MockEvent>(
890+
RpcResponse {
891+
context: RpcResponseContext::new(0),
892+
value: RpcLogsResponse {
893+
signature: "".to_string(),
894+
err: None,
895+
logs: logs.to_vec(),
896+
},
897+
},
898+
program_id_str,
899+
)
900+
.unwrap();
901+
902+
Ok(())
903+
}
871904
}

ts/packages/anchor/src/program/event.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ export class EventManager {
167167
export class EventParser {
168168
private coder: Coder;
169169
private programId: PublicKey;
170+
private static readonly INVOKE_RE =
171+
/^Program ([1-9A-HJ-NP-Za-km-z]+) invoke \[(\d+)\]$/;
172+
private static readonly ROOT_DEPTH = "1";
170173

171174
constructor(programId: PublicKey, coder: Coder) {
172175
this.coder = coder;
@@ -184,26 +187,43 @@ export class EventParser {
184187
// its emission, thereby allowing us to know if a given log event was
185188
// emitted by *this* program. If it was, then we parse the raw string and
186189
// emit the event if the string matches the event being subscribed to.
187-
public *parseLogs(logs: string[], errorOnDecodeFailure: boolean = false) {
188-
const logScanner = new LogScanner(logs);
190+
public *parseLogs(
191+
logs: string[],
192+
errorOnDecodeFailure = false
193+
): Generator<Event> {
194+
const scanner = new LogScanner([...logs]);
189195
const execution = new ExecutionContext();
190-
let log = logScanner.next();
191-
while (log !== null) {
196+
197+
const firstLog = scanner.next();
198+
if (firstLog === null) return;
199+
200+
const firstCap = EventParser.INVOKE_RE.exec(firstLog);
201+
if (!firstCap || firstCap[2] !== EventParser.ROOT_DEPTH) {
202+
throw new Error(`Unexpected first log line: ${firstLog}`);
203+
}
204+
execution.push(firstCap[1]);
205+
206+
while (scanner.peek() !== null) {
207+
const log = scanner.next();
208+
if (log === null) break;
209+
192210
let [event, newProgram, didPop] = this.handleLog(
193211
execution,
194212
log,
195213
errorOnDecodeFailure
196214
);
197-
if (event) {
198-
yield event;
199-
}
200-
if (newProgram) {
201-
execution.push(newProgram);
202-
}
215+
216+
if (event) yield event;
217+
if (newProgram) execution.push(newProgram);
218+
203219
if (didPop) {
204220
execution.pop();
221+
const nextLog = scanner.peek();
222+
if (nextLog && nextLog.endsWith("invoke [1]")) {
223+
const m = EventParser.INVOKE_RE.exec(nextLog);
224+
if (m) execution.push(m[1]);
225+
}
205226
}
206-
log = logScanner.next();
207227
}
208228
}
209229

@@ -254,23 +274,17 @@ export class EventParser {
254274

255275
// Handles logs when the current program being executing is *not* this.
256276
private handleSystemLog(log: string): [string | null, boolean] {
257-
// System component.
258-
const logStart = log.split(":")[0];
259-
260-
// Did the program finish executing?
261-
if (logStart.match(/^Program (.*) success/g) !== null) {
262-
return [null, true];
263-
// Recursive call.
264-
} else if (
265-
logStart.startsWith(`Program ${this.programId.toString()} invoke`)
266-
) {
277+
if (log.startsWith(`Program ${this.programId.toString()} log:`)) {
267278
return [this.programId.toString(), false];
268-
}
269-
// CPI call.
270-
else if (logStart.includes("invoke")) {
271-
return ["cpi", false]; // Any string will do.
279+
} else if (log.includes("invoke") && !log.endsWith("[1]")) {
280+
return ["cpi", false];
272281
} else {
273-
return [null, false];
282+
let regex = /^Program ([1-9A-HJ-NP-Za-km-z]+) success$/;
283+
if (regex.test(log)) {
284+
return [null, true];
285+
} else {
286+
return [null, false];
287+
}
274288
}
275289
}
276290
}
@@ -300,7 +314,12 @@ class ExecutionContext {
300314
}
301315

302316
class LogScanner {
303-
constructor(public logs: string[]) {}
317+
constructor(public logs: string[]) {
318+
// remove any logs that don't start with "Program "
319+
// this can happen in loader logs.
320+
// e.g. 3psYALQ9s7SjdezXw2kxKkVuQLtSAQxPAjETvy765EVxJE7cYqfc4oGbpNYEWsAiuXuTnqcsSUHLQ3iZUenTHTsA on devnet
321+
this.logs = this.logs.filter((log) => log.startsWith("Program "));
322+
}
304323

305324
next(): string | null {
306325
if (this.logs.length === 0) {
@@ -310,4 +329,8 @@ class LogScanner {
310329
this.logs = this.logs.slice(1);
311330
return l;
312331
}
332+
333+
peek(): string | null {
334+
return this.logs.length > 0 ? this.logs[0] : null;
335+
}
313336
}

ts/packages/anchor/tests/events.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,4 +315,40 @@ describe("Events", () => {
315315
throw new Error("Should never find logs");
316316
}
317317
});
318+
it("Handles malicious log injection attempts", () => {
319+
const logs = [
320+
"Program fake111111111111111111111111111111111111112 invoke [1]",
321+
"Program log: i logged success",
322+
"Program log: i logged success",
323+
"Program fake111111111111111111111111111111111111112 consumed 1411 of 200000 compute units",
324+
"Program fake111111111111111111111111111111111111112 success",
325+
];
326+
327+
const idl: Idl = {
328+
address: "Test111111111111111111111111111111111111111",
329+
metadata: {
330+
name: "basic_0",
331+
version: "0.0.0",
332+
spec: "0.1.0",
333+
},
334+
instructions: [
335+
{
336+
name: "initialize",
337+
accounts: [],
338+
args: [],
339+
discriminator: [],
340+
},
341+
],
342+
};
343+
const coder = new BorshCoder(idl);
344+
const programId = PublicKey.default;
345+
const eventParser = new EventParser(programId, coder);
346+
347+
// Should not find any events due to strict log parsing
348+
if (Array.from(eventParser.parseLogs(logs)).length > 0) {
349+
throw new Error(
350+
"Should never find logs with malicious injection attempts"
351+
);
352+
}
353+
});
318354
});

0 commit comments

Comments
 (0)