@@ -25,6 +25,18 @@ class NovaAPIServer {
2525 let port : UInt16 = 37423
2626 private var listener : NWListener ?
2727 private let startTime = Date ( )
28+
29+ /// Local-only anti-CSRF bearer token (not a secret — just prevents drive-by POST from browser JS)
30+ private let apiToken : String = {
31+ let key = " NovaAPIToken "
32+ if let existing = UserDefaults . standard. string ( forKey: key) , !existing. isEmpty {
33+ return existing
34+ }
35+ let token = UUID ( ) . uuidString
36+ UserDefaults . standard. set ( token, forKey: key)
37+ return token
38+ } ( )
39+
2840 private init ( ) { }
2941
3042 func start( ) {
@@ -56,6 +68,13 @@ class NovaAPIServer {
5668 private func route( _ req: NovaRequest ) async -> String {
5769 if req. method == " OPTIONS " { return http ( 200 , " " ) }
5870
71+ // Require bearer token for all POST requests (anti-CSRF)
72+ if req. method == " POST " {
73+ guard let auth = req. headers [ " authorization " ] , auth == " Bearer \( apiToken) " else {
74+ return json ( 401 , [ " error " : " Unauthorized — missing or invalid Bearer token " ] as [ String : Any ] )
75+ }
76+ }
77+
5978 switch ( req. method, req. path) {
6079
6180 case ( " GET " , " /api/status " ) :
@@ -90,12 +109,28 @@ class NovaAPIServer {
90109 guard let body = req. bodyJSON ( ) , let ip = body [ " ip " ] as? String else {
91110 return json ( 400 , [ " error " : " 'ip' required " ] as [ String : Any ] )
92111 }
93- // Scan via nmap subprocess
112+ // SECURITY: Validate IP/CIDR format to prevent command injection.
113+ // Only allow IPv4 addresses with optional CIDR notation (e.g. 192.168.1.0/24).
114+ let ipRegex = /^ \d { 1 , 3 } \. \d{ 1 , 3 } \. \d{ 1 , 3 } \. \d{ 1 , 3 } ( \/ \d { 1 , 2 } ) ? $/
115+ guard ip. wholeMatch( of: ipRegex) != nil else {
116+ return json ( 400 , [ " error " : " Invalid IP address format. Expected: x.x.x.x or x.x.x.x/n " ] as [ String : Any ] )
117+ }
118+ // Additionally validate each octet is 0-255
119+ let octets = ip. components ( separatedBy: " / " ) . first!. components ( separatedBy: " . " )
120+ let validOctets = octets. allSatisfy { if let n = Int ( $0) { return n >= 0 && n <= 255 } else { return false } }
121+ guard validOctets else {
122+ return json ( 400 , [ " error " : " Invalid IP address: octets must be 0-255 " ] as [ String : Any ] )
123+ }
124+ if let cidrPart = ip. components ( separatedBy: " / " ) . last, ip. contains ( " / " ) ,
125+ let cidr = Int ( cidrPart) , ( cidr < 0 || cidr > 32 ) {
126+ return json ( 400 , [ " error " : " Invalid CIDR prefix: must be 0-32 " ] as [ String : Any ] )
127+ }
128+ // Scan via nmap subprocess — arguments passed as array, never through shell
94129 Task {
95- let result = Process ( )
96- result . executableURL = URL ( fileURLWithPath: " /usr/bin/env " )
97- result . arguments = [ " nmap " , " -sV " , " --open " , ip]
98- try ? result . run ( )
130+ let nmapProcess = Process ( )
131+ nmapProcess . executableURL = URL ( fileURLWithPath: " /usr/local/ bin/nmap " )
132+ nmapProcess . arguments = [ " -sV " , " --open " , ip]
133+ try ? nmapProcess . run ( )
99134 }
100135 return json ( 200 , [ " status " : " scan_started " , " ip " : ip] as [ String : Any ] )
101136
@@ -208,7 +243,7 @@ class NovaAPIServer {
208243 }
209244
210245 private struct NovaRequest {
211- let method : String ; let path : String ; let body : String
246+ let method : String ; let path : String ; let body : String ; let headers : [ String : String ]
212247 func bodyJSON( ) -> [ String : Any ] ? { guard let d = body. data ( using: . utf8) else { return nil } ; return try ? JSONSerialization . jsonObject ( with: d) as? [ String : Any ] }
213248 init ? ( _ data: Data ) {
214249 guard let raw = String ( data: data, encoding: . utf8) , raw. contains ( " \r \n \r \n " ) else { return nil }
@@ -217,7 +252,7 @@ class NovaAPIServer {
217252 var hdrs : [ String : String ] = [ : ] ; for l in lines. dropFirst ( ) { let kv = l. components ( separatedBy: " : " ) ; if kv. count >= 2 { hdrs [ kv [ 0 ] . lowercased ( ) ] = kv. dropFirst ( ) . joined ( separator: " : " ) } }
218253 let rawBody = parts. dropFirst ( ) . joined ( separator: " \r \n \r \n " )
219254 if let cl = hdrs [ " content-length " ] , let n = Int ( cl) , rawBody. utf8. count < n { return nil }
220- method = tokens [ 0 ] ; path = tokens [ 1 ] . components ( separatedBy: " ? " ) . first ?? tokens [ 1 ] ; body = rawBody
255+ method = tokens [ 0 ] ; path = tokens [ 1 ] . components ( separatedBy: " ? " ) . first ?? tokens [ 1 ] ; body = rawBody; headers = hdrs
221256 }
222257 }
223258 // Map severity to STIX 2.1 indicator type
@@ -231,5 +266,5 @@ class NovaAPIServer {
231266
232267 private func json( _ s: Int , _ d: [ String : Any ] ) -> String { guard let data = try ? JSONSerialization . data ( withJSONObject: d, options: . prettyPrinted) , let body = String ( data: data, encoding: . utf8) else { return http ( 500 , " " ) } ; return http ( s, body, " application/json " ) }
233268 private func jsonArray( _ s: Int , _ a: [ [ String : Any ] ] ) -> String { guard let data = try ? JSONSerialization . data ( withJSONObject: a, options: . prettyPrinted) , let body = String ( data: data, encoding: . utf8) else { return http ( 500 , " " ) } ; return http ( s, body, " application/json " ) }
234- private func http( _ s: Int , _ body: String , _ ct: String = " text/plain " ) -> String { let st = [ 200 : " OK " , 201 : " Created " , 400 : " Bad Request " , 404 : " Not Found " , 500 : " Internal Server Error " ] [ s] ?? " Unknown " ; return " HTTP/1.1 \( s) \( st) \r \n Content-Type: \( ct) ; charset=utf-8 \r \n Content-Length: \( body. utf8. count) \r \n Access-Control-Allow-Origin: * \r \n Connection: close \r \n \r \n \( body) " }
269+ private func http( _ s: Int , _ body: String , _ ct: String = " text/plain " ) -> String { let st = [ 200 : " OK " , 201 : " Created " , 400 : " Bad Request " , 401 : " Unauthorized " , 404 : " Not Found " , 500 : " Internal Server Error " ] [ s] ?? " Unknown " ; return " HTTP/1.1 \( s) \( st) \r \n Content-Type: \( ct) ; charset=utf-8 \r \n Content-Length: \( body. utf8. count) \r \n Connection: close \r \n \r \n \( body) " }
235270}
0 commit comments