本教程是将以下内容中的概念付诸实践的四篇教程之一:2022年3月微服务专题:Kubernetes网络:
- 通过自动扩缩容降低Kubernetes延迟
- 通过速率限制保护Kubernetes API
- 保护Kubernetes应用免受SQL注入攻击(本文)
- 通过金丝雀部署提高正常运行时间和韧性
想了解如何使用NGINX应对更多Kubernetes网络用例的详细指南?下载免费电子书《使用NGINX管理Kubernetes流量:实用指南》。
您在当地一家颇受欢迎的商店从事IT工作,该店销售从枕头到自行车等各种商品。商店即将推出首家网店,但在网站上线前,他们请安全专家对其进行渗透测试。遗憾的是,安全专家发现了一个问题!该网店存在SQL注入漏洞。安全专家能够利用该网站的漏洞,从数据库中获取包括用户名和密码在内的敏感信息。
团队向身为Kubernetes工程师的您求助,希望您能解决这一难题。幸运的是,您知道可以使用Kubernetes流量管理工具来缓解SQL注入及其他漏洞。您已经部署了Ingress控制器来公开应用,只需一项配置,就能确保该漏洞无法被利用。现在,在线商店可以按时上线了。做得好!
实验与教程概述
本博客是“2022年3月微服务”第3单元实验的配套内容,主题为“Kubernetes中的微服务安全模式”,演示如何使用NGINX和NGINX Ingress Controller阻止SQL注入。
要运行本教程,需要准备一台满足以下条件的计算机:
- 2 个或更多 CPU
- 2 GB可用内存
- 20 GB可用磁盘空间
- 互联网连接
- 容器或虚拟机管理器,例如Docker、Hyperkit、Hyper-V、KVM、Parallels、Podman、VirtualBox或VMware Fusion/Workstation
- minikube 已安装
- Helm 已安装
- 一种可启动浏览器窗口的配置。如果无法启动,则需要确定如何通过浏览器访问相关服务。
为充分利用实验和教程,建议在开始前:
- 观看概念概述直播录像
- 查看背景博客、网络研讨会和视频
- 观看这段16分钟的实验室内容摘要视频:
 
本教程使用以下技术:
- NGINX Open Source
- NGINX Ingress Controller(基于NGINX Open Source)
- BusyBox
- Helm
- minikube
- 一个存在安全漏洞的简单应用,专为本实验开发
每项挑战的说明都包含用于配置应用的YAML文件全文。也可以从我们的GitHub存储库复制这些文本。每个YAML文件的文本旁均提供GitHub链接。
本教程包含4项挑战:
挑战 1:部署集群和易受攻击的应用
在此挑战中,您将部署minikube集群,并安装Podinfo,将其用作存在安全漏洞的示例应用。
创建Minikube集群
部署一个minikube集群。几秒钟后,系统会显示一条消息,确认部署成功。
$ minikube start
🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default安装存在漏洞的应用
在此部署一个由两个微服务组成的简单电子商务应用:
- MariaDB数据库
- 连接数据库并检索数据的PHP微服务
执行以下步骤:
- 使用您选择的文本编辑器创建一个名为1-app.yaml的YAML文件,并填入以下内容(也可以从GitHub复制)。
apiVersion: apps/v1 kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 80
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db - 部署应用和API:
$ kubectl apply -f 1-app.yaml deployment.apps/app created
service/app created
deployment.apps/db created
service/db created - 确认Podinfo Pod已部署,判断依据是
Running值显示在STATUS列中。Pod可能需要30–40秒才能完成部署,因此请等待两个Pod的状态均变为Running,再继续下一步(必要时可重新运行该命令)。
$ kubectl get podsNAME READY STATUS RESTARTS AGE
app-d65d9b879-b65f2 1/1 Running 0 37s
db-7bbcdc75c-q2kt5 1/1 Running 0 37s - 在浏览器中打开应用:
$ minikube service app|-----------|------|-------------|--------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|--------------|
| default | app | | No node port |
|-----------|------|-------------|--------------|
😿 service default/app has no node port
🏃 Starting tunnel for service app.
|-----------|------|-------------|------------------------|
| NAMESPACE | NAME | TARGET PORT | URL |
|-----------|------|-------------|------------------------|
| default | app | | http://127.0.0.1:55446 |
|-----------|------|-------------|------------------------|
🎉 Opening service default/app in default browser... 挑战2:入侵应用
该示例应用程序较为基础。它包含一个列出商品(例如枕头)的主页,以及一组显示商品描述和价格等详细信息的商品页面。数据存储在MariaDB数据库中。每次请求页面时,系统都会向数据库发出一条SQL查询。
- 对于首页,系统会检索数据库中的所有项目。
- 对于产品页面,按ID获取相应项目。
打开pillows产品页面时,您可能会注意到URL以/product/1结尾。其中,1是该产品的ID。为防止恶意代码直接插入SQL查询,建议先清理用户输入,再将请求转发到后端服务。但如果应用配置不当,输入在插入针对数据库的SQL查询之前未进行转义,会发生什么?
利用漏洞 1
要确定应用是否正确转义输入,可进行一个简单的实验:将ID更改为数据库中不存在的值。
手动将URL中的最后一个元素从1更改为-1。错误消息Invalid product id "-1"表明产品ID未经过转义,而是将该字符串直接插入查询中。除非您是黑客,否则这可不是好事!

假设数据库查询如下:
SELECT * FROM some_table WHERE id = "1"要利用因未转义输入而导致的漏洞,请将1替换为-1" <malicious_query> -- //,如下所示:
- 位于
"之后的引号(-1)使第一个查询完整闭合。 - 可以在引号后添加自己的恶意查询。
- The
--//序列会丢弃查询的其余部分。
例如,如果将URL中的最后一个元素更改为‑1"或1 -- //,查询将编译为:
SELECT * FROM some_table WHERE id = "-1" OR 1 -- //"
--------------
^ injected ^ 这会选择数据库中的所有行,对入侵很有帮助。要确认是否属于这种情况,请将URL结尾改为‑1"。随后显示的错误消息会提供更多有关该数据库的实用信息:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '"-1""' at line 1 in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23现在,可以开始修改注入的代码,尝试按ID对数据库结果进行排序:
-1" OR 1 ORDER BY id DESC -- //结果是数据库中最后一项的产品页面。
漏洞利用 2
强制数据库对结果进行排序很有意思,但如果目标是实施入侵,这并没有太大用处。尝试从数据库中提取用户名和密码更值得投入精力。
可以合理假设,数据库中有一个包含用户名和密码的用户表。但如何将访问范围从产品表扩展到用户表?
答案是注入如下代码:
-1" UNION SELECT * FROM users -- //其中
‑1"会强制第一个查询返回空集。UNION强制合并两个数据库表,在本例中为products和users,从而可以获取原始表(products)中没有的信息(密码)。SELECT*FROMusers会选择 users 表中的所有行。- The
--//序列会丢弃恶意查询之后的所有内容。
将URL末尾修改为注入的代码后,会出现一条新的错误消息:
Fatal error: Uncaught mysqli_sql_exception: The used SELECT statements have a different number of columns in /var/www/html/product.php:23 Stack trace: #0 /var/www/html/product.php(23): mysqli->query('SELECT * FROM p...') #1 {main} thrown in /var/www/html/product.php on line 23此消息表明,products表和users表的列数不同,因此无法执行UNION指令。不过,可以通过反复尝试来确定列数:每次添加一列(字段名称),并将其作为参数传递给SELECT指令。可以合理猜测,users表中有一个名为password的字段,因此请尝试使用该字段:
# select 1 column-1" UNION SELECT password FROM users; -- //
# select 2 columns
-1" UNION SELECT password,password FROM users; -- //
# select 3 columns
-1" UNION SELECT password,password,password FROM users; -- /
# select 4 columns
-1" UNION SELECT password,password,password,password FROM users; -- //
# select 5 columns
-1" UNION SELECT password,password,password,password,password FROM users; -- //最后一个查询成功(这表明users表中有5列),并且您会看到一个用户密码:

此时,您还不知道与此密码对应的用户名。但由于已知users表的列数,可以使用与之前相同类型的查询来获取该信息。假设相关字段名为username。事实证明这一假设是正确的,以下查询会同时暴露users表中的用户名和密码。这当然很好,除非此应用托管在您的基础设施上!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- //
挑战3:使用NGINX边车容器阻止特定请求
在线商店应用的开发人员显然需要更加重视用户输入清理(例如使用参数化查询),但作为Kubernetes工程师,您也可以通过阻止攻击到达应用来帮助防范SQL注入。这样一来,即使应用存在漏洞,其影响也不会那么大。
保护应用的方法有很多。在本实验的其余部分,我们将重点介绍以下两种:
- 在本挑战中,需要在Pod中注入一个边车容器,以代理所有流量,并拒绝URL中包含
UNION的任何请求。首先,将NGINX Open Source部署为边车,然后测试它能否过滤恶意查询。注意:此处采用这项技术仅用于演示。实际上,手动将代理部署为边车并非理想的解决方案(稍后将详细介绍)。 - 在挑战 4中,我们使用NGINX Ingress Controller过滤进入集群的所有流量。
将NGINX Open Source部署为边车容器
- 创建一个名为2-app-sidecar.yaml的YAML文件,并添加以下内容(也可以从GitHub复制)。该配置的要点包括:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: f5devcentral/microservicesmarch:1.0.3
ports:
- containerPort: 80
env:
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
- name: DATABASE_HOSTNAME
value: db.default.svc.cluster.local
- name: proxy # <-- sidecar
image: "nginx"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /etc/nginx
name: nginx-config
volumes:
- name: nginx-config
configMap:
name: sidecar
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
ports:
- port: 80
targetPort: 8080 # <-- the traffic is routed to the proxy
nodePort: 30001
selector:
app: app
type: NodePort
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sidecar
data:
nginx.conf: |-
events {}
http {
server {
listen 8080 default_server;
listen [::]:8080 default_server;
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
location / {
proxy_pass http://localhost:80/;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
spec:
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mariadb:10.3.32-focal
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
value: root
- name: MYSQL_USER
value: dan
- name: MYSQL_PASSWORD
value: dan
- name: MYSQL_DATABASE
value: sqlitraining
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
ports:
- port: 3306
targetPort: 3306
selector:
app: db- 在端口8080上启动一个运行NGINX Open Source的边车容器。
- NGINX将所有流量转发到应用。
- 任何包含以下字符(以及其他字符)的请求都会被拒绝:
SELECT或UNION(请参阅ConfigMap部分中的第一个location块)。 - 该应用的服务会先将所有流量路由到NGINX容器。
- 部署边车:
$ kubectl apply -f 2-app-sidecar.yaml deployment.apps/app configured
service/app configured
configmap/sidecar created
deployment.apps/db unchanged
service/db unchanged 测试作为过滤器的Sidecar
返回应用并再次尝试SQL注入,以测试Sidecar是否正在过滤流量。NGINX会在请求到达应用之前将其阻止!
-1" UNION SELECT username,username,password,password,username FROM users where id=1 -- // 
挑战4:配置NGINX Ingress Controller以筛选请求
按照挑战 3中的方式保护应用,是一次有趣的学习实践,但不建议在生产环境中使用,原因如下:
- 这并非完整的安全解决方案。
- 它不具备可扩展性(无法轻松将这种保护应用于多个应用)。
- 更新过程复杂且效率低下。
更好的解决方案是使用NGINX Ingress Controller,将同样的保护扩展到所有应用!Ingress控制器可用于集中管理各种安全功能,包括像Web应用防火墙(WAF)一样阻止请求,以及进行身份验证和授权。

在本挑战中,您将部署NGINX Ingress Controller、配置流量路由,并验证过滤器能否阻止SQL注入攻击。
部署NGINX Ingress Controller
安装NGINX Ingress Controller最快捷的方式是使用Helm。
- 将NGINX仓库添加到Helm:
$ helm repo add nginx-stable https://helm.nginx.com/stable - 下载并安装基于NGINX Open Source的NGINX Ingress Controller,该控制器由F5 NGINX维护。请注意
enableSnippets=true参数:代码片段用于配置NGINX以阻止SQL注入。输出的最后一行确认安装成功。
$ helm install main nginx-stable/nginx-ingress \ --set controller.watchIngressWithoutClass=true --set controller.service.type=NodePort \ --set controller.service.httpPort.nodePort=30005 \ --set controller.enableSnippets=true
NAME: main
LAST DEPLOYED: Day Mon DD hh:mm:ss YYYY
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES: The NGINX Ingress Controller has been installed. - 确认NGINX Ingress Controller Pod已部署,判断依据是
Running值显示在STATUS列中。
$ kubectl get pods NAME READY STATUS ...
main-nginx-ingress-779b74bb8b-mtdkr 1/1 Running ...
... RESTARTS AGE
... 0 18s将流量路由到您的应用
- 创建一个名为3-ingress.yaml的YAML文件,并添加以下内容(也可以从GitHub复制)。该文件定义了将流量路由到应用所需的Ingress清单(这次不经过边车代理)。请注意
annotations:块,其中使用代码片段来自定义NGINX Ingress Controller配置,并采用与挑战3的ConfigMap定义中相同的location块:它会拒绝包含SELECT或UNION等字符串的任何请求。
apiVersion: v1 kind: Service
metadata:
name: app-without-sidecar
spec:
ports:
- port: 80
targetPort: 80
selector:
app: app
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: entry
annotations:
nginx.org/server-snippets: |
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
deny all;
}
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: app-without-sidecar
port:
number: 80
path: /
pathType: Prefix - 部署Ingress资源:
$ kubectl apply -f 3-ingress.yaml service/app-without-sidecar created
ingress.networking.k8s.io/entry created 验证过滤器运行情况
- 启动一个一次性BusyBox容器,使用正确的主机名向NGINX Ingress Controller Pod发送请求。
$ kubectl run -ti --rm=true busybox --image=busybox$ wget --header="Host: example.com" -qO- main-nginx-ingress
<!DOCTYPE html>
<html lang="en">
<head>
# ...- 尝试进行SQL注入。
403Forbidden状态码表明NGINX已阻止此次攻击!
$ wget --header="Host: example.com" -qO- 'main-nginx-ingress/product/-1"%20UNION%20SELECT%20username,username,password,password,username%20FROM%20users%20where%2 0id=1%20--%20//'
wget: server returned error: HTTP/1.1 403 Forbidden 后续步骤
Kubernetes默认并不安全。Ingress控制器可以缓解SQL注入及许多其他漏洞。但请注意,刚刚使用NGINX Ingress Controller实现的这类WAF功能无法取代真正的WAF,也不能替代安全的应用架构设计。经验丰富的黑客只需对代码稍作修改,仍可利用UNION实施攻击。有关此主题的更多信息,请参阅《渗透测试人员SQL注入(SQLi)指南》。
尽管如此,Ingress控制器仍是集中管理大部分安全功能的强大工具,有助于提高效率和安全性,可用于集中式身份验证和授权(mTLS、单点登录),还可集成强大的WAF,例如F5 NGINX App Protect WAF。
应用和架构的复杂性可能需要更精细的控制。如果贵组织需要零信任和端到端加密,可以考虑使用永久免费提供的服务网格,例如F5 NGINX Service Mesh,以控制Kubernetes集群中各服务之间的通信(东西向流量)。我们将在第4单元高级Kubernetes部署策略中探讨服务网格。
有关获取和部署NGINX Open Source的详细信息,请访问nginx.org。
要试用基于NGINX Plus并集成NGINX App Protect的NGINX Ingress Controller,请立即开始30天免费试用,或联系我们,讨论您的用例。
要试用基于NGINX Open Source的NGINX Ingress Controller,请在我们的GitHub存储库中查看“NGINX Ingress Controller版本”,或从“DockerHub”下载预构建容器。
相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。

