feat: initial codeql setup (#1390)

This commit is contained in:
Eli Bosley
2025-05-14 20:21:52 -04:00
committed by GitHub
parent e580f646a5
commit 2ade7eb527
8 changed files with 500 additions and 0 deletions

49
.github/codeql/README.md vendored Normal file
View File

@@ -0,0 +1,49 @@
# CodeQL Security Analysis for Unraid API
This directory contains custom CodeQL queries and configurations for security analysis of the Unraid API codebase.
## Overview
The analysis is configured to run:
- On all pushes to the main branch
- On all pull requests
- Weekly via scheduled runs
## Custom Queries
The following custom queries are implemented:
1. **API Authorization Bypass Detection**
Identifies API handlers that may not properly check authorization before performing operations.
2. **GraphQL Injection Detection**
Detects potential injection vulnerabilities in GraphQL queries and operations.
3. **Hardcoded Secrets Detection**
Finds potential hardcoded secrets or credentials in the codebase.
4. **Insecure Cryptographic Implementations**
Identifies usage of weak cryptographic algorithms or insecure random number generation.
5. **Path Traversal Vulnerability Detection**
Detects potential path traversal vulnerabilities in file system operations.
## Configuration
The CodeQL analysis is configured in:
- `.github/workflows/codeql-analysis.yml` - Workflow configuration
- `.github/codeql/codeql-config.yml` - CodeQL engine configuration
## Running Locally
To run these queries locally:
1. Install the CodeQL CLI: https://github.com/github/codeql-cli-binaries/releases
2. Create a CodeQL database:
```
codeql database create <db-name> --language=javascript --source-root=.
```
3. Run a query:
```
codeql query run .github/codeql/custom-queries/javascript/api-auth-bypass.ql --database=<db-name>
```

16
.github/codeql/codeql-config.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: "Unraid API CodeQL Configuration"
disable-default-queries: false
queries:
- name: Extended Security Queries
uses: security-extended
- name: Custom Unraid API Queries
uses: ./.github/codeql/custom-queries
query-filters:
- exclude:
problem.severity:
- warning
- recommendation
tags contain: security

View File

@@ -0,0 +1,45 @@
/**
* @name Potential API Authorization Bypass
* @description Functions that process API requests without verifying authorization may lead to security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision medium
* @id js/api-auth-bypass
* @tags security
* external/cwe/cwe-285
*/
import javascript
/**
* Identifies functions that appear to handle API requests
*/
predicate isApiHandler(Function f) {
exists(f.getAParameter()) and
(
f.getName().regexpMatch("(?i).*(api|handler|controller|resolver|endpoint).*") or
exists(CallExpr call |
call.getCalleeName().regexpMatch("(?i).*(get|post|put|delete|patch).*") and
call.getArgument(1) = f
)
)
}
/**
* Identifies expressions that appear to perform authorization checks
*/
predicate isAuthCheck(DataFlow::Node node) {
exists(CallExpr call |
call.getCalleeName().regexpMatch("(?i).*(authorize|authenticate|isAuth|checkAuth|verifyAuth|hasPermission|isAdmin|canAccess).*") and
call.flow().getASuccessor*() = node
)
}
from Function apiHandler
where
isApiHandler(apiHandler) and
not exists(DataFlow::Node authCheck |
isAuthCheck(authCheck) and
authCheck.getEnclosingExpr().getEnclosingFunction() = apiHandler
)
select apiHandler, "API handler function may not perform proper authorization checks."

View File

@@ -0,0 +1,77 @@
/**
* @name Potential GraphQL Injection
* @description User-controlled input used directly in GraphQL queries may lead to injection vulnerabilities.
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/graphql-injection
* @tags security
* external/cwe/cwe-943
*/
import javascript
import DataFlow::PathGraph
class GraphQLQueryExecution extends DataFlow::CallNode {
GraphQLQueryExecution() {
exists(string name |
name = this.getCalleeName() and
(
name = "execute" or
name = "executeQuery" or
name = "query" or
name.regexpMatch("(?i).*graphql.*query.*")
)
)
}
DataFlow::Node getQuery() {
result = this.getArgument(0)
}
}
class UserControlledInput extends DataFlow::Node {
UserControlledInput() {
exists(DataFlow::ParameterNode param |
param.getName().regexpMatch("(?i).*(query|request|input|args|variables|params).*") and
this = param
)
or
exists(DataFlow::PropRead prop |
prop.getPropertyName().regexpMatch("(?i).*(query|request|input|args|variables|params).*") and
this = prop
)
}
}
/**
* Holds if `node` is a string concatenation.
*/
predicate isStringConcatenation(DataFlow::Node node) {
exists(BinaryExpr concat |
concat.getOperator() = "+" and
concat.flow() = node
)
}
class GraphQLInjectionConfig extends TaintTracking::Configuration {
GraphQLInjectionConfig() { this = "GraphQLInjectionConfig" }
override predicate isSource(DataFlow::Node source) {
source instanceof UserControlledInput
}
override predicate isSink(DataFlow::Node sink) {
exists(GraphQLQueryExecution exec | sink = exec.getQuery())
}
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
// Add any GraphQL-specific taint steps if needed
isStringConcatenation(succ) and
succ.(DataFlow::BinaryExprNode).getAnOperand() = pred
}
}
from GraphQLInjectionConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "GraphQL query may contain user-controlled input from $@.", source.getNode(), "user input"

View File

@@ -0,0 +1,53 @@
/**
* @name Hardcoded Secrets
* @description Hardcoded secrets or credentials in source code can lead to security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision medium
* @id js/hardcoded-secrets
* @tags security
* external/cwe/cwe-798
*/
import javascript
/**
* Identifies variable declarations or assignments that may contain secrets
*/
predicate isSensitiveAssignment(DataFlow::Node node) {
exists(DataFlow::PropWrite propWrite |
propWrite.getPropertyName().regexpMatch("(?i).*(secret|key|password|token|credential|auth).*") and
propWrite.getRhs() = node
)
or
exists(VariableDeclarator decl |
decl.getName().regexpMatch("(?i).*(secret|key|password|token|credential|auth).*") and
decl.getInit().flow() = node
)
}
/**
* Identifies literals that look like secrets
*/
predicate isSecretLiteral(StringLiteral literal) {
// Match alphanumeric strings of moderate length that may be secrets
literal.getValue().regexpMatch("[A-Za-z0-9_\\-]{8,}") and
not (
// Skip likely non-sensitive literals
literal.getValue().regexpMatch("(?i)^(true|false|null|undefined|localhost|development|production|staging)$") or
// Skip URLs without credentials
literal.getValue().regexpMatch("^https?://[^:@/]+")
)
}
from DataFlow::Node source
where
isSensitiveAssignment(source) and
(
exists(StringLiteral literal |
literal.flow() = source and
isSecretLiteral(literal)
)
)
select source, "This assignment may contain a hardcoded secret or credential."

View File

@@ -0,0 +1,90 @@
/**
* @name Insecure Cryptographic Implementation
* @description Usage of weak cryptographic algorithms or improper implementations can lead to security vulnerabilities.
* @kind problem
* @problem.severity error
* @precision high
* @id js/insecure-crypto
* @tags security
* external/cwe/cwe-327
*/
import javascript
/**
* Identifies calls to crypto functions with insecure algorithms
*/
predicate isInsecureCryptoCall(CallExpr call) {
// Node.js crypto module uses
exists(string methodName |
methodName = call.getCalleeName() and
(
// Detect MD5 usage
methodName.regexpMatch("(?i).*md5.*") or
methodName.regexpMatch("(?i).*sha1.*") or
// Insecure crypto constructors
(
methodName = "createHash" or
methodName = "createCipheriv" or
methodName = "createDecipher"
) and
(
exists(StringLiteral algo |
algo = call.getArgument(0) and
(
algo.getValue().regexpMatch("(?i).*(md5|md4|md2|sha1|des|rc4|blowfish).*") or
algo.getValue().regexpMatch("(?i).*(ecb).*") // ECB mode
)
)
)
)
)
or
// Browser crypto API uses
exists(MethodCallExpr mce, string propertyName |
propertyName = mce.getMethodName() and
(
propertyName = "subtle" and
exists(MethodCallExpr subtleCall |
subtleCall.getReceiver() = mce and
subtleCall.getMethodName() = "encrypt" and
exists(ObjectExpr obj |
obj = subtleCall.getArgument(0) and
exists(Property p |
p = obj.getAProperty() and
p.getName() = "name" and
exists(StringLiteral algo |
algo = p.getInit() and
algo.getValue().regexpMatch("(?i).*(rc4|des|aes-cbc).*")
)
)
)
)
)
)
}
/**
* Identifies usage of Math.random() for security-sensitive operations
*/
predicate isInsecureRandomCall(CallExpr call) {
exists(PropertyAccess prop |
prop.getPropertyName() = "random" and
prop.getBase().toString() = "Math" and
call.getCallee() = prop
)
}
from Expr insecureExpr, string message
where
(
insecureExpr instanceof CallExpr and
isInsecureCryptoCall(insecureExpr) and
message = "Using potentially insecure cryptographic algorithm or mode."
) or (
insecureExpr instanceof CallExpr and
isInsecureRandomCall(insecureExpr) and
message = "Using Math.random() for security-sensitive operation. Consider using crypto.getRandomValues() instead."
)
select insecureExpr, message

View File

@@ -0,0 +1,130 @@
/**
* @name Path Traversal Vulnerability
* @description User-controlled inputs used in file operations may allow for path traversal attacks.
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/path-traversal
* @tags security
* external/cwe/cwe-22
*/
import javascript
import DataFlow::PathGraph
/**
* Identifies sources of user-controlled input
*/
class UserInput extends DataFlow::Node {
UserInput() {
// HTTP request parameters
exists(DataFlow::ParameterNode param |
param.getName().regexpMatch("(?i).*(req|request|param|query|body|user|input).*") and
this = param
)
or
// Access to common request properties
exists(DataFlow::PropRead prop |
(
prop.getPropertyName() = "query" or
prop.getPropertyName() = "body" or
prop.getPropertyName() = "params" or
prop.getPropertyName() = "files"
) and
this = prop
)
}
}
/**
* Identifies fs module imports
*/
class FileSystemAccess extends DataFlow::CallNode {
FileSystemAccess() {
// Node.js fs module functions
exists(string name |
name = this.getCalleeName() and
(
name = "readFile" or
name = "readFileSync" or
name = "writeFile" or
name = "writeFileSync" or
name = "appendFile" or
name = "appendFileSync" or
name = "createReadStream" or
name = "createWriteStream" or
name = "openSync" or
name = "open"
)
)
or
// File system operations via require('fs')
exists(DataFlow::SourceNode fsModule, string methodName |
(fsModule.getAPropertyRead("promises") or fsModule).flowsTo(this.getReceiver()) and
methodName = this.getMethodName() and
(
methodName = "readFile" or
methodName = "writeFile" or
methodName = "appendFile" or
methodName = "readdir" or
methodName = "stat"
)
)
}
DataFlow::Node getPathArgument() {
result = this.getArgument(0)
}
}
/**
* Identifies sanitization of file paths
*/
predicate isPathSanitized(DataFlow::Node node) {
// Check for path normalization or validation
exists(DataFlow::CallNode call |
(
call.getCalleeName() = "resolve" or
call.getCalleeName() = "normalize" or
call.getCalleeName() = "isAbsolute" or
call.getCalleeName() = "relative" or
call.getCalleeName().regexpMatch("(?i).*(sanitize|validate|check).*path.*")
) and
call.flowsTo(node)
)
or
// Check for path traversal mitigation patterns
exists(DataFlow::CallNode call |
call.getCalleeName() = "replace" and
exists(StringLiteral regex |
regex = call.getArgument(0).(DataFlow::RegExpCreationNode).getSource().getAChildExpr() and
regex.getValue().regexpMatch("(\\.\\./|\\.\\.\\\\)")
) and
call.flowsTo(node)
)
}
/**
* Configuration for tracking flow from user input to file system operations
*/
class PathTraversalConfig extends TaintTracking::Configuration {
PathTraversalConfig() { this = "PathTraversalConfig" }
override predicate isSource(DataFlow::Node source) {
source instanceof UserInput
}
override predicate isSink(DataFlow::Node sink) {
exists(FileSystemAccess fileAccess |
sink = fileAccess.getPathArgument()
)
}
override predicate isSanitizer(DataFlow::Node node) {
isPathSanitized(node)
}
}
from PathTraversalConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "File system operation depends on a user-controlled value $@.", source.getNode(), "user input"

40
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: "CodeQL Security Analysis"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * 0' # Run weekly on Sundays
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'typescript' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql/codeql-config.yml
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2