Skip to content

Commit 43d5ef8

Browse files
committed
New rule for Go unrestricted bind
Checks crypto/tls use of Listen with an address of INADDR_ANY or IN6ADDR_ANY. Note: currently doesn't properly handle the address argument to the call if given as a variable. Rule ID is GO005 Signed-off-by: Eric Brown <eric.brown@securesauce.dev>
1 parent 4e54a0f commit 43d5ef8

File tree

6 files changed

+299
-0
lines changed

6 files changed

+299
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
id: GO005
3+
title: crypto — unrestricted bind
4+
hide_title: true
5+
pagination_prev: null
6+
pagination_next: null
7+
slug: /rules/GO005
8+
---
9+
10+
::: precli.rules.go.stdlib.crypto_unrestricted_bind
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Copyright 2025 Secure Sauce LLC
2+
# SPDX-License-Identifier: BUSL-1.1
3+
r"""
4+
# Binding to an Unrestricted IP Address in `crypto` Package
5+
6+
Sockets can be bound to the IPv4 address `0.0.0.0` or IPv6 equivalent of
7+
`[::]`, which configures the socket to listen for incoming connections on all
8+
network interfaces. While this can be intended in environments where
9+
services are meant to be publicly accessible, it can also introduce significant
10+
security risks if the service is not intended for public or wide network
11+
access.
12+
13+
Binding a socket to `0.0.0.0` or `[::]` can unintentionally expose the
14+
application to the wider network or the internet, making it accessible from
15+
any interface. This exposure can lead to unauthorized access, data breaches,
16+
or exploitation of vulnerabilities within the application if the service is
17+
not adequately secured or if the binding is unintended. Restricting the socket
18+
to listen on specific interfaces limits the exposure and reduces the attack
19+
surface.
20+
21+
# Example
22+
23+
```go linenums="1" hl_lines="18" title="crypto_tls_listen_ipv4.go"
24+
package main
25+
26+
import (
27+
"crypto/tls"
28+
"log"
29+
)
30+
31+
func main() {
32+
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
33+
if err != nil {
34+
log.Fatalf("failed to load key pair: %v", err)
35+
}
36+
37+
config := &tls.Config{
38+
Certificates: []tls.Certificate{cert},
39+
}
40+
41+
ln, err := tls.Listen("tcp", "0.0.0.0:8443", config)
42+
if err != nil {
43+
log.Fatalf("tls.Listen failed on %s: %v", addr, err)
44+
}
45+
defer ln.Close()
46+
}
47+
```
48+
49+
??? example "Example Output"
50+
```
51+
> precli tests/unit/rules/go/stdlib/crypto/examples/crypto_tls_listen_ipv4.go
52+
⚠️ Warning on line 18 in tests/unit/rules/go/stdlib/crypto/examples/crypto_tls_listen_ipv4.go
53+
GO005: Binding to an Unrestricted IP Address
54+
Binding to 'INADDR_ANY (0.0.0.0)' exposes the application on all network interfaces, increasing the risk of unauthorized access.
55+
```
56+
57+
# Remediation
58+
59+
All socket bindings MUST specify a specific network interface or localhost
60+
(127.0.0.1/localhost for IPv4, [::1] for IPv6) unless the application is
61+
explicitly designed to be accessible from any network interface. This
62+
practice ensures that services are not exposed more broadly than intended.
63+
64+
```go linenums="1" hl_lines="18" title="crypto_tls_listen_ipv4.go"
65+
package main
66+
67+
import (
68+
"crypto/tls"
69+
"log"
70+
)
71+
72+
func main() {
73+
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
74+
if err != nil {
75+
log.Fatalf("failed to load key pair: %v", err)
76+
}
77+
78+
config := &tls.Config{
79+
Certificates: []tls.Certificate{cert},
80+
}
81+
82+
ln, err := tls.Listen("tcp", "127.0.0.1:8443", config)
83+
if err != nil {
84+
log.Fatalf("tls.Listen failed on %s: %v", addr, err)
85+
}
86+
defer ln.Close()
87+
}
88+
```
89+
90+
# Default Configuration
91+
92+
```toml
93+
enabled = true
94+
level = "warning"
95+
```
96+
97+
# See also
98+
99+
!!! info
100+
- [tls package - crypto_tls - Go Packages](https://pkg.go.dev/crypto/tls#Listen)
101+
- [CWE-1327: Binding to an Unrestricted IP Address](https://cwe.mitre.org/data/definitions/1327.html)
102+
103+
_New in version 0.8.1_
104+
105+
""" # noqa: E501
106+
from typing import Optional
107+
108+
from precli.core import utils
109+
from precli.core.call import Call
110+
from precli.core.location import Location
111+
from precli.core.result import Result
112+
from precli.i18n import _
113+
from precli.rules import Rule
114+
115+
116+
INADDR_ANY = "0.0.0.0"
117+
IN6ADDR_ANY = "[::]"
118+
119+
120+
class CryptoUnrestrictedBind(Rule):
121+
def __init__(self, id: str):
122+
super().__init__(
123+
id=id,
124+
name="unrestricted_bind",
125+
description=__doc__,
126+
cwe_id=1327,
127+
message=_(
128+
"Binding to '{0}' exposes the application on all network "
129+
"interfaces, increasing the risk of unauthorized access."
130+
),
131+
)
132+
133+
def analyze_call_expression(
134+
self, context: dict, call: Call
135+
) -> Optional[Result]:
136+
if call.name_qualified not in ("crypto/tls.Listen",):
137+
return
138+
139+
arg = call.get_argument(position=1, name="laddr")
140+
# TODO: Go needs to have string argument support
141+
# if not arg.is_str:
142+
# return
143+
144+
laddr = arg.value
145+
if ":" in laddr:
146+
laddr = tuple(laddr.rsplit(":", 1))
147+
else:
148+
laddr = (laddr,)
149+
150+
# In Go, "" instructs to bind to all IPv4 and IPv6 addresses if
151+
# a dual-stack OS. There is no localhost equivalent.
152+
if utils.to_str(laddr[0]) in ("", INADDR_ANY):
153+
fixes = Rule.get_fixes(
154+
context=context,
155+
deleted_location=Location(node=arg.node),
156+
description=_(
157+
"Use the localhost address to restrict binding."
158+
),
159+
inserted_content=f'"127.0.0.1:{laddr[1]}"',
160+
)
161+
return Result(
162+
rule_id=self.id,
163+
location=Location(node=arg.node),
164+
message=self.message.format("INADDR_ANY (0.0.0.0)"),
165+
fixes=fixes,
166+
)
167+
if utils.to_str(laddr[0]) == IN6ADDR_ANY:
168+
fixes = Rule.get_fixes(
169+
context=context,
170+
deleted_location=Location(node=arg.node),
171+
description=_(
172+
"Use the localhost address to restrict binding."
173+
),
174+
inserted_content=f'"[::1]:{laddr[1]}"',
175+
)
176+
return Result(
177+
rule_id=self.id,
178+
location=Location(node=arg.node),
179+
message=self.message.format("IN6ADDR_ANY ([::])"),
180+
fixes=fixes,
181+
)

setup.cfg

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ precli.rules.go =
8787
# precli/rules/go/stdlib/syscall_setuid_root.py
8888
GO004 = precli.rules.go.stdlib.syscall_setuid_root:SyscallSetuidRoot
8989

90+
# precli/rules/go/stdlib/crypto_unrestricted_bind.py
91+
GO005 = precli.rules.go.stdlib.crypto_unrestricted_bind:CryptoUnrestrictedBind
92+
9093
precli.rules.java =
9194
# precli/rules/java/stdlib/javax_crypto_weak_cipher.py
9295
JAV001 = precli.rules.java.stdlib.javax_crypto_weak_cipher:WeakCipher
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// level: WARNING
2+
// start_line: 23
3+
// end_line: 23
4+
// start_column: 33
5+
// end_column: 47
6+
package main
7+
8+
import (
9+
"crypto/tls"
10+
"log"
11+
)
12+
13+
func main() {
14+
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
15+
if err != nil {
16+
log.Fatalf("failed to load key pair: %v", err)
17+
}
18+
19+
config := &tls.Config{
20+
Certificates: []tls.Certificate{cert},
21+
}
22+
23+
ln, err := tls.Listen("tcp", "0.0.0.0:8443", config)
24+
if err != nil {
25+
log.Fatalf("tls.Listen failed on %s: %v", addr, err)
26+
}
27+
defer ln.Close()
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// level: WARNING
2+
// start_line: 23
3+
// end_line: 23
4+
// start_column: 33
5+
// end_column: 44
6+
package main
7+
8+
import (
9+
"crypto/tls"
10+
"log"
11+
)
12+
13+
func main() {
14+
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
15+
if err != nil {
16+
log.Fatalf("failed to load key pair: %v", err)
17+
}
18+
19+
config := &tls.Config{
20+
Certificates: []tls.Certificate{cert},
21+
}
22+
23+
ln, err := tls.Listen("tcp", "[::]:8443", config)
24+
if err != nil {
25+
log.Fatalf("tls.Listen failed on %s: %v", addr, err)
26+
}
27+
defer ln.Close()
28+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2025 Secure Sauce LLC
2+
# SPDX-License-Identifier: BUSL-1.1
3+
import os
4+
5+
import pytest
6+
7+
from precli.core.level import Level
8+
from precli.parsers import go
9+
from precli.rules import Rule
10+
from tests.unit.rules import test_case
11+
12+
13+
class TestCryptoUnrestrictedBind(test_case.TestCase):
14+
@classmethod
15+
def setup_class(cls):
16+
cls.rule_id = "GO005"
17+
cls.parser = go.Go()
18+
cls.base_path = os.path.join(
19+
"tests",
20+
"unit",
21+
"rules",
22+
"go",
23+
"stdlib",
24+
"crypto",
25+
"examples",
26+
)
27+
28+
def test_rule_meta(self):
29+
rule = Rule.get_by_id(self.rule_id)
30+
assert rule.id == self.rule_id
31+
assert rule.name == "unrestricted_bind"
32+
assert (
33+
rule.help_url
34+
== f"https://docs.securesauce.dev/rules/{self.rule_id}"
35+
)
36+
assert rule.config.enabled is True
37+
assert rule.config.level == Level.WARNING
38+
assert rule.config.rank == -1.0
39+
assert rule.cwe.id == 1327
40+
41+
@pytest.mark.parametrize(
42+
"filename",
43+
[
44+
"crypto_tls_listen_ipv4.go",
45+
"crypto_tls_listen_ipv6.go",
46+
],
47+
)
48+
def test(self, filename):
49+
self.check(filename)

0 commit comments

Comments
 (0)