Files
codeql/swift/ql/test/query-tests/Security/CWE-094/UnsafeJsEval.swift
2024-10-29 20:36:16 +01:00

337 lines
10 KiB
Swift

// --- stubs ---
class NSObject {}
@MainActor class UIResponder : NSObject {}
@MainActor class UIView : UIResponder {}
@MainActor class NSResponder : NSObject {}
class NSView : NSResponder {}
class WKFrameInfo : NSObject {}
class WKContentWorld : NSObject {
class var defaultClient: WKContentWorld { WKContentWorld() }
class var page: WKContentWorld { WKContentWorld() }
}
class WKWebView : UIView {
func evaluateJavaScript(
_ javaScriptString: String
) async throws -> Any { "" }
func evaluateJavaScript(
_ javaScriptString: String,
completionHandler: ((Any?, Error?) -> Void)? = nil
) {
completionHandler?(nil, nil)
}
@MainActor func evaluateJavaScript(
_ javaScript: String,
in frame: WKFrameInfo? = nil,
in contentWorld: WKContentWorld,
completionHandler: ((Result<Any, Error>) -> Void)? = nil
) {
completionHandler?(.success(""))
}
@MainActor func evaluateJavaScript(
_ javaScript: String,
in frame: WKFrameInfo? = nil,
contentWorld: WKContentWorld
) async throws -> Any? { nil }
@MainActor func callAsyncJavaScript(
_ functionBody: String,
arguments: [String : Any] = [:],
in frame: WKFrameInfo? = nil,
in contentWorld: WKContentWorld,
completionHandler: ((Result<Any, Error>) -> Void)? = nil
) {
completionHandler?(.success(""))
}
@MainActor func callAsyncJavaScript(
_ functionBody: String,
arguments: [String : Any] = [:],
in frame: WKFrameInfo? = nil,
contentWorld: WKContentWorld
) async throws -> Any? { nil }
}
enum WKUserScriptInjectionTime : Int, @unchecked Sendable {
case atDocumentStart, atDocumentEnd
}
class WKUserScript : NSObject {
init(
source: String,
injectionTime: WKUserScriptInjectionTime,
forMainFrameOnly: Bool
) {}
init(
source: String,
injectionTime: WKUserScriptInjectionTime,
forMainFrameOnly: Bool,
in contentWorld: WKContentWorld
) {}
}
class WKUserContentController : NSObject {
func addUserScript(_ userScript: WKUserScript) {}
}
class UIWebView : UIView {
// deprecated
func stringByEvaluatingJavaScript(from script: String) -> String? { nil }
}
class WebView : NSView {
// deprecated
func stringByEvaluatingJavaScript(from script: String!) -> String! { "" }
}
class JSValue : NSObject {}
class JSContext {
func evaluateScript(_ script: String!) -> JSValue! { return JSValue() }
func evaluateScript(
_ script: String!,
withSourceURL sourceURL: URL!
) -> JSValue! { return JSValue() }
}
typealias JSContextRef = OpaquePointer
typealias JSStringRef = OpaquePointer
typealias JSObjectRef = OpaquePointer
typealias JSValueRef = OpaquePointer
typealias JSChar = UInt16
func JSStringCreateWithCharacters(
_ chars: UnsafePointer<JSChar>!,
_ numChars: Int
) -> JSStringRef! {
return chars.withMemoryRebound(to: CChar.self, capacity: numChars) {
cchars in OpaquePointer(cchars)
}
}
func JSStringCreateWithUTF8CString(_ string: UnsafePointer<CChar>!) -> JSStringRef! {
return OpaquePointer(string)
}
func JSStringRetain(_ string: JSStringRef!) -> JSStringRef! { return string }
func JSStringRelease(_ string: JSStringRef!) { }
func JSEvaluateScript(
_ ctx: JSContextRef!,
_ script: JSStringRef!,
_ thisObject: JSObjectRef!,
_ sourceURL: JSStringRef!,
_ startingLineNumber: Int32,
_ exception: UnsafeMutablePointer<JSValueRef?>!
) -> JSValueRef! { return OpaquePointer(bitPattern: 0) }
@frozen
public struct Data: Collection {
public typealias Index = Int
public typealias Element = UInt8
public subscript(x: Index) -> Element { 0 }
public var startIndex: Index { 0 }
public var endIndex: Index { 0 }
public func index(after i: Index) -> Index { i + 1 }
init<S>(_ elements: S) {}
}
struct URL {
init?(string: String) {}
init?(string: String, relativeTo: URL?) {}
}
extension String {
init(contentsOf: URL) throws {
let data = ""
// ...
self.init(data)
}
}
// --- tests ---
func testAsync(_ sink: @escaping (String) async throws -> ()) {
Task {
let localString = "console.log('localString')"
let localStringFragment = "'localStringFragment'"
let url = URL(string: "http://example.com/")
try! await sink(localString) // GOOD: the HTML data is local
try! await sink(try String(contentsOf: URL(string: "http://example.com/")!)) // $ MISSING: Alert (HTML contains remote input, may access local secrets)
try! await sink(try! String(contentsOf: url!)) // $ MISSING: Alert
try! await sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local
try! await sink("console.log(" + (try! String(contentsOf: url!)) + ")") // $ MISSING: Alert
let localData = Data(localString.utf8)
let remoteData = Data((try! String(contentsOf: url!)).utf8)
try! await sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local
try! await sink(String(decoding: remoteData, as: UTF8.self)) // $ MISSING: Alert the data is remote
try! await sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion
try! await sink("console.log(" + String(Int(try! String(contentsOf: url!)) ?? 0) + ")") // GOOD: Primitive conversion
try! await sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
try! await sink("console.log(" + ((try! String(contentsOf: url!)).count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
}
}
func testSync(_ sink: @escaping (String) -> ()) {
let localString = "console.log('localString')"
let localStringFragment = "'localStringFragment'"
let url = URL(string: "http://example.com/")
sink(localString) // GOOD: the HTML data is local
sink(try! String(contentsOf: URL(string: "http://example.com/")!)) // $ Source=source1
sink(try! String(contentsOf: url!)) // $ Source=source2
sink("console.log(" + localStringFragment + ")") // GOOD: the HTML data is local
sink("console.log(" + (try! String(contentsOf: url!)) + ")") // $ Source=source3
let localData = Data(localString.utf8)
let remoteData = Data((try! String(contentsOf: url!)).utf8) // $ Source=source4
sink(String(decoding: localData, as: UTF8.self)) // GOOD: the data is local
sink(String(decoding: remoteData, as: UTF8.self))
sink("console.log(" + String(Int(localStringFragment) ?? 0) + ")") // GOOD: Primitive conversion
sink("console.log(" + String(Int(try! String(contentsOf: url!)) ?? 0) + ")") // GOOD: Primitive conversion
sink("console.log(" + (localStringFragment.count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
sink("console.log(" + ((try! String(contentsOf: url!)).count != 0 ? "1" : "0") + ")") // GOOD: Primitive conversion
}
func testUIWebView() {
let webview = UIWebView()
testAsync { string in
_ = await webview.stringByEvaluatingJavaScript(from: string) // $ MISSING: Alert
}
}
func testWebView() {
let webview = WebView()
testAsync { string in
_ = await webview.stringByEvaluatingJavaScript(from: string) // $ MISSING: Alert
}
}
func testWKWebView() {
let webview = WKWebView()
testAsync { string in
_ = try await webview.evaluateJavaScript(string) // $ MISSING: Alert
}
testAsync { string in
await webview.evaluateJavaScript(string) { _, _ in } // $ MISSING: Alert
}
testAsync { string in
await webview.evaluateJavaScript(string, in: nil, in: WKContentWorld.defaultClient) { _ in } // $ MISSING: Alert
}
testAsync { string in
_ = try await webview.evaluateJavaScript(string, contentWorld: .defaultClient) // $ MISSING: Alert
}
testAsync { string in
await webview.callAsyncJavaScript(string, in: nil, in: .defaultClient) { _ in () } // $ MISSING: Alert
}
testAsync { string in
_ = try await webview.callAsyncJavaScript(string, contentWorld: WKContentWorld.defaultClient) // $ MISSING: Alert
}
}
func testWKUserContentController() {
let ctrl = WKUserContentController()
testSync { string in
ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentStart, forMainFrameOnly: false)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
}
testSync { string in
ctrl.addUserScript(WKUserScript(source: string, injectionTime: .atDocumentEnd, forMainFrameOnly: true, in: .defaultClient)) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
}
}
func testJSContext() {
let ctx = JSContext()
testSync { string in
_ = ctx.evaluateScript(string) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
}
testSync { string in
_ = ctx.evaluateScript(string, withSourceURL: URL(string: "https://example.com")) // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
}
}
func testJSEvaluateScript() {
testSync { string in
string.utf16.withContiguousStorageIfAvailable { stringBytes in
let jsstr = JSStringRetain(JSStringCreateWithCharacters(stringBytes.baseAddress, string.count))
defer { JSStringRelease(jsstr) }
_ = JSEvaluateScript(
/*ctx:*/ OpaquePointer(bitPattern: 0),
/*script:*/ jsstr, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
/*thisObject:*/ OpaquePointer(bitPattern: 0),
/*sourceURL:*/ OpaquePointer(bitPattern: 0),
/*startingLineNumber:*/ 0,
/*exception:*/ UnsafeMutablePointer(bitPattern: 0)
)
}
}
testSync { string in
string.utf8CString.withUnsafeBufferPointer { stringBytes in
let jsstr = JSStringRetain(JSStringCreateWithUTF8CString(stringBytes.baseAddress))
defer { JSStringRelease(jsstr) }
_ = JSEvaluateScript(
/*ctx:*/ OpaquePointer(bitPattern: 0),
/*script:*/ jsstr, // $ Alert=source1 $ Alert=source2 $ Alert=source3 $ Alert=source4
/*thisObject:*/ OpaquePointer(bitPattern: 0),
/*sourceURL:*/ OpaquePointer(bitPattern: 0),
/*startingLineNumber:*/ 0,
/*exception:*/ UnsafeMutablePointer(bitPattern: 0)
)
}
}
}
func testQHelpExamples() {
Task {
let webview = WKWebView()
let remoteData = try String(contentsOf: URL(string: "http://example.com/evil.json")!) // $ Source=source5
_ = try await webview.evaluateJavaScript("console.log(" + remoteData + ")") // $ Alert=source5
_ = try await webview.callAsyncJavaScript(
"console.log(data)",
arguments: ["data": remoteData], // GOOD
contentWorld: .page
)
}
}
testUIWebView()
testWebView()
testWKWebView()
testWKUserContentController()
testJSContext()
testJSEvaluateScript()
testQHelpExamples()