summaryrefslogtreecommitdiff
path: root/compiler/Methods/WebRequest.cs
blob: b7f3f90a084ce039a971b5ee8d0cc605778fc07d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
using AtLangCompiler.ILEmitter;
using System.Net;
using System.Reflection.Emit;

namespace AtLangCompiler.Methods;

internal class WebRequestToken : ILexerTokenConfig
{
    public IReadOnlyDictionary<string, TokenType> TokenStrings => new Dictionary<string, TokenType>
    {
        { "@getWeb", TokenType.GETWEB },
        { "@postWeb", TokenType.POSTWEB }
    };
}

internal class WebRequestAssign : ASTNode
{
    public string VarName { get; }
    public string Method { get; }
    public string UrlVar { get; }
    public string? BodyVar { get; }
    public string? PostVar { get; }

    public WebRequestAssign(string varName, string method, string url, string? body, string? post)
    {
        VarName = varName;
        Method = method;
        UrlVar = url;
        BodyVar = body;
        PostVar = post;
    }
}

[ParserFor(TokenType.GETWEB, inputTokens: 1)]
[ParserFor(TokenType.POSTWEB, inputTokens: 2)]
internal class WebRequestParser : IStatementParser
{
    public ASTNode ParseStatement(Parser parser)
    {
        if (parser.current.Type == TokenType.GETWEB)
        {
            parser.Eat(TokenType.GETWEB);
            parser.Eat(TokenType.LPAREN);
            parser.Eat(TokenType.AT);
            string url = parser.current.Text;
            parser.Eat(TokenType.IDENT);
            parser.Eat(TokenType.RPAREN);
            return new WebRequestAssign(parser.varName, "GET", url, null, null);
        }
        else if (parser.current.Type == TokenType.POSTWEB)
        {
            parser.Eat(TokenType.POSTWEB);
            parser.Eat(TokenType.LPAREN);
            parser.Eat(TokenType.AT);
            string url = parser.current.Text;
            parser.Eat(TokenType.IDENT);
            parser.Eat(TokenType.AT);
            string postData = parser.current.Text;
            parser.Eat(TokenType.IDENT);
            parser.Eat(TokenType.RPAREN);
            return new WebRequestAssign(parser.varName, "POST", url, null, postData);
        }
        else
        {
            throw new ArgumentOutOfRangeException($"Unknown token {parser.current.Type}");
        }
    }
}

[EmitterFor(typeof(WebRequestAssign))]
internal class WebRequest : IMethodEmitter<WebRequestAssign>
{
    private readonly ILGenerator il;
    private readonly LocalBuilder dictLocal;

    public WebRequest(ILGenerator il, LocalBuilder dictLocal)
    {
        this.il = il ?? throw new ArgumentNullException(nameof(il));
        this.dictLocal = dictLocal ?? throw new ArgumentNullException(nameof(dictLocal));
    }

    public void EmitIL(WebRequestAssign node)
    {
        // dict[node.VarName] = new WebClient().DownloadString( dict[node.URL] )  (for GET)
        // or dict[node.VarName] = new WebClient().UploadString( dict[node.URL], dict[node.BodyVar] )  (for POST, if body is also a variable)

        // 1) Load dictLocal
        il.Emit(OpCodes.Ldloc, dictLocal);

        // 2) Push node.VarName (the key in the dictionary where we store the response)
        il.Emit(OpCodes.Ldstr, node.VarName);

        // 3) Create new WebClient
        il.Emit(OpCodes.Newobj, typeof(WebClient).GetConstructor(Type.EmptyTypes)!);

        // 4) Because node.URL is a variable name, we must do a dictionary lookup to get the actual URL string:
        il.Emit(OpCodes.Ldloc, dictLocal);
        il.Emit(OpCodes.Ldstr, node.UrlVar);  // the *name* of the variable
        System.Reflection.MethodInfo dictGetItem = typeof(Dictionary<string, object>)
            .GetProperty("Item")!
            .GetGetMethod()!;
        il.Emit(OpCodes.Isinst, typeof(string));
        il.Emit(OpCodes.Callvirt, dictGetItem); // returns the actual URL string

        // SSRF validation: block requests to internal/restricted addresses
        LocalBuilder urlLocal = il.DeclareLocal(typeof(string));
        il.Emit(OpCodes.Castclass, typeof(string));
        il.Emit(OpCodes.Stloc, urlLocal);

        LocalBuilder uriLocal = il.DeclareLocal(typeof(Uri));
        il.Emit(OpCodes.Ldloc, urlLocal);
        il.Emit(OpCodes.Newobj, typeof(Uri).GetConstructor([typeof(string)])!);
        il.Emit(OpCodes.Stloc, uriLocal);

        LocalBuilder hostLocal = il.DeclareLocal(typeof(string));
        il.Emit(OpCodes.Ldloc, uriLocal);
        il.Emit(OpCodes.Callvirt, typeof(Uri).GetProperty("Host")!.GetGetMethod()!);
        il.Emit(OpCodes.Stloc, hostLocal);

        Label blockedLabel = il.DefineLabel();
        Label urlSafeLabel = il.DefineLabel();

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "localhost");
        il.Emit(OpCodes.Call, typeof(string).GetMethod("op_Equality", [typeof(string), typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "127.0.0.1");
        il.Emit(OpCodes.Call, typeof(string).GetMethod("op_Equality", [typeof(string), typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, ".local");
        il.Emit(OpCodes.Callvirt, typeof(string).GetMethod("EndsWith", [typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "10.");
        il.Emit(OpCodes.Callvirt, typeof(string).GetMethod("StartsWith", [typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "192.168.");
        il.Emit(OpCodes.Callvirt, typeof(string).GetMethod("StartsWith", [typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "172.16.");
        il.Emit(OpCodes.Callvirt, typeof(string).GetMethod("StartsWith", [typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Ldloc, hostLocal);
        il.Emit(OpCodes.Ldstr, "169.254.169.254");
        il.Emit(OpCodes.Call, typeof(string).GetMethod("op_Equality", [typeof(string), typeof(string)])!);
        il.Emit(OpCodes.Brtrue, blockedLabel);

        il.Emit(OpCodes.Br, urlSafeLabel);

        il.MarkLabel(blockedLabel);
        il.Emit(OpCodes.Ldstr, "Blocked: URL points to restricted address");
        il.Emit(OpCodes.Newobj, typeof(Exception).GetConstructor([typeof(string)])!);
        il.Emit(OpCodes.Throw);

        il.MarkLabel(urlSafeLabel);

        // Reload validated URL onto the stack
        il.Emit(OpCodes.Ldloc, urlLocal);

        if (node.Method.Equals(nameof(HttpMethod.Get), StringComparison.OrdinalIgnoreCase))
        {
            // WebClient.DownloadString(string)
            System.Reflection.MethodInfo downloadString = typeof(WebClient).GetMethod(nameof(WebClient.DownloadString), [typeof(string)])!;
            il.Emit(OpCodes.Callvirt, downloadString);
        }
        else if (node.Method.Equals(nameof(HttpMethod.Post), StringComparison.OrdinalIgnoreCase))
        {
            il.Emit(OpCodes.Ldloc, dictLocal);
            il.Emit(OpCodes.Ldstr, node.PostVar!);
            il.Emit(OpCodes.Callvirt, dictGetItem);  // now the stack has: [webClient, urlString, bodyString]

            // WebClient.UploadString(string url, string data)
            System.Reflection.MethodInfo uploadString = typeof(WebClient)
                .GetMethod(nameof(WebClient.UploadString), [typeof(string), typeof(string)])!;
            il.Emit(OpCodes.Callvirt, uploadString);
        }
        else
        {
            // If the method is unknown, push a dummy string or error message
            il.Emit(OpCodes.Pop); // discard the WebClient instance
            il.Emit(OpCodes.Ldstr, $"Unsupported HTTP method: {node.Method}");
        }

        // At this point, the top of the stack is the response string (or error message).
        // 5) Store it in dict[node.VarName]
        System.Reflection.MethodInfo dictSetItem = typeof(Dictionary<string, object>)
            .GetProperty("Item")!
            .GetSetMethod()!;
        il.Emit(OpCodes.Isinst, typeof(string));
        il.Emit(OpCodes.Callvirt, dictSetItem);
    }
}