I have two very complex queries that are displayed on the same form and that use multiple parameters supplied by that Form. I would like to reuse the query in a VBA function and/or on another form. As such, I have two questions:
Is there a way to write the SQL statement such that a query dynamically determines the form to read its parameter from rather than having to specify the name of the form? E.g., Something along the line of Me!startDate, rather than Forms!myForm!startDate.
Is there a way to supply parameters to a query when opening it as a DAO RecordSet?
For the most part, Jet (Access) is not subject to the same injection problems that other databases experience, so it may suit to write the SQL using VBA and either update a query with the new sql, or set the form's record source.
Very roughly:
sSQL = "SELECT f1, f2 FROM tbl WHERE f3 = '" & Me.txt3 & "'"
CurrentDB.QueryDefs("aquery").SQL = sSQL
Alternatively, you can use parameters:
Query:
PARAMETERS txtCompany Text(150);
SELECT <...>
WHERE Company = txtCompany
Code:
Set qdf = db.QueryDefs("QueryName")
qdf.Parameters!txtCompany = Trim(frm!txtCompany)
I almost never define parameters or store references to form/report controls in saved QueryDefs. To me, those should be supplied at runtime where you use the saved QueryDef.
In general, I write my SQL on the fly in code, rather than using saved QueryDefs.
Also, keep in mind that you can set the Recordsource of a form in its OnOpen event, which means you can use conditions there to decide on what filtering you'd like for the specific purpose. This can be determined based on outside forms, or using the OpenArgs parameter of DoCmd.OpenForm.
Related
The general idea was to create a VBA script that effectively write SQL SELECT queries for users that don't have experience with SQL. It accepts from a database that they're already familiar with. The ultimate goal is to allow users to:
Select what columns they would like to see.
Make simple restrictions on them (date range, specific part numbers)
Order this list however they would like.
I have written (in VBA) a script to do so, but I have one remaining issue. I can't execute SELECT queries directly in VBA. I also can't find any information on how to export the completed query from VBA back into the Access Database. I've considered writing to a text file, then having Access read from said text file, using a macro, and importing the query, but it does not appear that Access support such a functionality..
Any help would be greatly appreciated.
Dim sql_ as String
sql_ = "SELECT * FROM Table"
DoCmd.RunSQL sql_
Dim sql_ as String
sql_ = "UPDATE Table SET Field = 'ABC'"
CurrentDb.Execute sql_, dbFailOnError
Here is how I would tackle this myself:
Write the VBA to build the SQL string (which you've done)
Create a data connection to your Access DB (Data > Get External Data > From Access)
Use VBA to change the command text in the data connection properties, then refresh.
This should enable you to dynamically build SQL, query the DB and return the results to Excel in a table.
Hope that helps!
I am very new to MS Access, forgive me for this simple question but I am very confused with my current problem.
So I want to run a VBA function after a table receives an update on one of its fields. What I have done is:
Create a Macro named Macro_update under CREATE->Macro, with action RunCode, and its argument is the VBA function I wish to run. The function has no bug.
Select my table, and under Table->After Update, I wrote
IF [Old].[status]=0 And [status]=1 THEN
RunDataMacro
MacroName Macro_update
But after I update my status field in my table nothing happened... I am suspicious of the fact that in step 2 my action is RunDataMacro, but I am actually running a Macro (is there a difference?)... any help is appreciated!
You can use a Data Macro to get it working locally for now. This means that the table will need to be stored in an Access database.
If your web service is not actually using the Access Runtime to interface with the access database container, then the data macros may not fire correctly nor as intended. Your mileage may vary.
If you later migrate your database to a SQL server (MySQL, Microsoft SQL, PostgreSQL) then your data macros will need to be implemented natively on the SQL server as a Trigger.
For now, I'm writing some instructions below to demonstrate how to call a VBA function from a Data Macro locally within a single Access database:
Create the VBA Function This will be the function that you want to call from the data Macro.
Create this in a Module, not in a Form or Class Module.
This has to be a function and cannot be a sub
Code:
Public Function VBAFunction(OldValue As String, NewValue As String) As String
Debug.Print "Old: " & Chr(34) & OldValue & Chr(34);
Debug.Print vbTab;
Debug.Print "New: " & Chr(34) & NewValue & Chr(34)
VBAFunction = "Worked"
End Function
Create the Data Macro (Going to be more descriptive here since people get lost here easy)
Open the Table (i.e. TestTable) in Design View
Find the correct Ribbon
In table design view, there is a contextual ribbon called Design.
On that ribbon, there is an option called Create Data Macros
Click on Create Data Macros and select After Update
The Macro Designer window should now open
Choose SetLocalVar from the Add New Action combo box
A SetLocalVar section appears.
In this section, I see Name and Expression
Set Name to an arbitrary value, such as: MyLocalVar
Set Expression to the following
Be sure to type the = sign, which will result in two equal signs being shown
Expression Text:
=VBAFunction([Old].[FieldName],[FieldName])
Save the Data Macro and Close the Macro Designer.
Save the Table and Close the Table
Test It: Create an Update Query
Next you will need to create an Update Query that performs an update on the Table that houses the Data Macro you just created.
To test this, you can just update a string field in all records to one value.
UPDATE [TestTable] SET [TestText] = "Test"
Run the query
Press Control + G to bring up the Immediate Window. You will notice that the Data Macro fired for every updated record.
I am the maintainer (but thankfully not the creator) of a very old, very large and very badly written classic ASP site for an electronics manufacturer.
Security is a joke. This is the only thing done to sanitize input before throwing it into the mouth of MySQL:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = trim(txtval)
End Function
productid = txtval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Set rs = conn.execute(SQL)
Because of that, the site is unfortunately (but perhaps not surprisingly) victim of SQL injection attacks, some of them succesful.
The simple means taken above is not nearly enough. Nor is using Server.HTMLEncode. Escaping slashes doesn't help either as the attacks are quite sophisticated:
product.asp?id=999999.9+UnIoN+AlL+SeLeCt+0x393133353134353632312e39,0x393133353134353632322e39,0x393133353134353632332e39,0x393133353134353632342e39,0x393133353134353632352e39,0x393133353134353632362e39
The url above (an arbitrary attempt taken from the access log) gives the folling response from the site:
Microsoft OLE DB Provider for ODBC Drivers error '80004005'
[MySQL][ODBC 5.3(w) Driver][mysqld-5.1.42-community]
The used SELECT statements have a different number of columns
/product.asp, line 14
This means that the injection made it through but in this case did not succeed in getting any data. Others do, however.
The site consists of hundreds of ASP files with spaghetti code summing up to many thousands of lines without much structure. Because of that it is not an option to go for parameterized queries. The work would be enormous and error prone as well.
One good thing though is that all input parameters in the code are consistently passed through the txtval function, so here is a chance to do it better by augmenting the function. Also, since all SQL calls are done with conn.execute(SQL) it is quite straightforward to search and replace with eg. conn.execute(sanitize(SQL)) so here is a chance to do something about it too.
Given the circumstances, what are my options to prevent or at least minimize the risc of SQL injection?
Any input is much appreciated.
Updates:
1.
I do understand that parameterized queries is the correct way to handle the problem. I use that myself when I create websites. But given the way the site is built and the size of it, it will take 1-2 months to modify, test and debug it. Even if that is what we end up with (which I doubt) I need to do something right now.
2.
The replacement with the html entity is not a typo. It replaces single quote with its html entity. (I didn't make the code!)
3.
In the specific example above, using CInt(id) would solve the problem, but it could be anything, not only numerical inputs.
UPDATE 2:
Ok, I know that I am not asking for the correct solution. I knew that from the start. That's why I wrote "Given the circumstances".
But still, filtering inputs for mysql keywords like select, union etc would at least make it better. Not good, but a little bit better. And this is what I am asking for, ideas to make it a little bit better.
Although I appreciate your comments, telling me that the only good option is to use parameterized queries doesn't really help. Because I know that already :)
I wouldn't give up on parameterized queries. They are the single best tool you can use to protect yourself from SQL Injection. If your plan is to replace all of these calls:
conn.execute(SQL)
to these calls:
conn.execute(sanitize(SQL))
then you're already looking at modifying each interaction with SQL (BTW, don't forget Command.Execute() and Recordset.Open(), which may also be used to run SQL statements). And since you're already planning on changing these calls, consider calling a custom function to run the statement. For example, replace:
set rs = conn.execute(SQL)
with:
set rs = MyExecute(SQL)
and then use your custom function to set up a proper parameterized query using a Command object instead. You'll need to cleverly parse the SQL statement in this custom function. Identify the values in the where clause, determine their type (perhaps you can query the table schema), and add parameters accordingly. But it can be done.
You can also take this opportunity to sanitize the input. Use a RegExp object to quickly strip [^0-9\.] from numeric fields, for example.
But there's still the opportunity that you'll return a recordset from this function that will be used to write values directly to the page without being HTML-encoded first. That's a real concern, especially since it sounds like your site has already been targeted in the past. I wouldn't trust any data coming from your database. The only option I see here (that wouldn't involve touching every page) is to return a "clean", HTML-encoded recordset instead of the default one.
Unfortunately, you're still not out of the woods. XSS attacks can be done via QueryString parameters, cookies, and form controls. How safe are you going to feel after "fixing" the SQL Injection issues knowing that XSS is still a very real possibility?
My advice? Explain to your supervisor the security threats plaguing your site and convince him/her the need for a thorough review or a complete rewrite. It may seem like a lot of resources to throw at an "old, already-working website", but the moment someone defaces your website or truncates your database tables, you'll wish you invested the time.
This attack should only affect numeric values passed in your SQL.
There may or may not be a quick fix depending on whether the same txtval function is used for both numeric and string values (and others like date too).
If txtval is only used for numeric values (probably unlikely) then you could protected by adding single quotes around the value, eg:
Function txtval(data)
txtval = replace(data,"'","'")
txtval = "'" & trim(txtval) & "'"
End Function
If it is used for all value types then your only option might be to search through all the code and either:
1) Add single quotes to all numeric SQL, eg:
SQL = "SELECT * FROM products WHERE id = '" & productid & "'"
2) Create a new function just for sanitizing number values and then change all your queries to use that (not a quick fix), eg:
Function numval(data)
If IsNumeric(data) Then
numvalue = CDbl(data)
Else
numvalue = 0 'or NULL?
End If
End Function
And then change your queries, eg:
productid = numval(Request.QueryString("id"))
SQL = "SELECT * FROM products WHERE id = " & productid
Is there common code (ie. in an include file) that is used to open the database and create the conn variable used in your sample code?
If so, then you could just replace that code and create your own class with Open, Close and Execute functions (at least). You may need other methods too if they are used in your code.
That way you could effectively override the execute in lines like Set rs = conn.execute(SQL).
Eg:
Class MyDatabase
Private m_conn
Public Sub Open(connString)
Set m_conn = Server.CreateObject("ADODB.Connection")
m_conn.Open connString
End Sub
Public Sub Close()
m_conn.Close
Set m_conn = Nothing
End Sub
Public Function Execute(sql)
'Sanitize input here (sql), simple example just for this type of attack
If InStr(sql, "UnIoN AlL SeLeCt") <> 0 Then sql = ""
'return a RecordSet
Set Execute = m_conn.Execute(sql)
End Property
End Class
Then change your common conn declaration from... (eg)
Set conn = Server.CreateObject("ADODB.Connection")
...to...
Set conn = New MyDatabase
If you keep the txtval function I would also update it to escape slashes as well as single quotes, eg:
Function txtval(data)
txtval = Replace(Replace(strValue, "'", "''"), "\", "\\")
txtval = trim(txtval)
End Function
Hopefully something here might be of help.
I have this qry in access, if I go into its design it has a criteria (which as I understand it is a parameter).
The Report this qry is based off of works great, click on it a little thing pops up asks the required info and off it goes. In code I am trying to do this and get a
Run-time error '424'
Object Required
the offending line:
qdf.Parameters("Insurance Name").Value = inputStr
Lines before it:
Set qfd = CurrentDb.QueryDefs("qryInsGrpRoster")
Dim inputStr As String
inputStr = InputBox("Enter Insurance")
'Supply the parameter value
qdf.Parameters("Insurance Name").Value = inputStr
inputStr definitely equals the value, it fails though.
The criteria line in the qry is:
Like "*" & [Insurance Name] & "*"
Do I need the likes and all that to set that parameter?
in Access 2010 and 2013
This uses DAO and might be of interest
DIM MyQryDef as querydef
Dim a as string
a = ""
a = a & "PARAMETERS Parameter1 INT, Parameter2 INT; "
a = a & "SELECT f1, f2 FROM atable WHERE "
a = a & "f3 = [Parameter1] AND f4 = [Parameter2] "
a = a & ";"
Set MyQryDef = currentdb().CreateQueryDef("MyQueryName", a)
MyQryDef.Parameters("Parameter1").Value = 33
MyQryDef.Parameters("Parameter2").Value = 2
' You could now use MyQryDef with DAO recordsets
' to use it with any of OpenQuery, BrowseTo , OpenForm, OpenQuery, OpenReport, or RunDataMacro
DoCmd.SetParameter "Parameter1", 33
DoCmd.SetParameter "Parameter2", 2
DoCmd.Form YourFormName
' or
DoCmd.SetParameter "Parameter1", 33
DoCmd.SetParameter "Parameter2", 2
DoCmd.OpenQuery MyQryDef.Name
See here:
https://msdn.microsoft.com/en-us/library/office/ff194182(v=office.14).aspx
Harvey
The parameters property of an Access Query is read only.
You have basically two options here that I can think of right off.
The first is to just completely rewrite the SQL of the saved query each time you need to use it. You can see an example of this here: How to change querydef sql programmatically in MS Access
The second option is to manually set the RecordSource of the report each time it opens. Using this method you will not use a saved query at all. You'll need to set/store the entire SQL statement in your code when the report opens, ask for any input from the user and append the input you get to your SQL statement. You could setup a system where the base SQL is stored in a table instead but, for simplicity, that's not necessary to achieve what you're trying to do here.
MS Access does allow you to use parametrized queries in the manner you're attempting here (not the same code you have), but as far as I know, it would require you to use Stored Procedures in MS SQL Server or MySQL and then you'd need to use ADO. One big downside is that Access reports cannot be bound to ADO recordsets so this isn't really an option for what you're trying to do in this particular instance.
Seems like a typo. You're creating the object named 'qfd', and trying to use the object named 'qdf'
Set qfd = ...
and then
qdf.Para...
I like to put Option Explicit in my modules to help me find these types of issues.
Lets say you are designing a sales report in microsoft access. You have 2 parameters: Startdate and EndDate.
I can think of 3 ways to prompt the end user for these parameters when the report is run.
Create a form with 2 Text Boxes and a button. The Button runs the report, and the report refers back to the form by name to get the start date and end date.
Create a form with 2 text boxes and a button. The button runs the report but sets the appropriate filter via the docmd.openreport command.
Base the report on a Query and define query parameters. Access will automatically prompt for those parameters one by one.
Which is the best way to go?
Which is best depends on a number of factors. First off, if you want to run the report without parameters, you don't want to define them in the recordsource of the report. This is also the problem with your first suggestion, which would tie the report to the form (from Access/Jet's point of view, there is little difference between a PARAMETER declared in the SQL of the recordsource and a form control reference; indeed, if you're doing it right, any time you use a form control reference, you will define it as a parameter!).
Of the three, the most flexible is your second suggestion, which means the report can be run without needing to open the form and without needing to supply the parameters at runtime.
You've left out one possibility that's somewhat in between the two, and that's to use the Form's OnOpen event to set the recordsource at runtime. To do that, you'd open the form where you're collecting your dates using the acDialog argument, hide the form after filling it out, and then write the recordsource on the fly. Something like this:
Private Sub Report_Open(Cancel As Integer)
Dim dteStart as Date
Dim dteEnd As Date
DoCmd.OpenForm "dlgGetDates", , , , , acDialog
If IsLoaded("dlgGetDates") Then
With Forms!dlgGetDates
dteStart = !StartDate
dteEnd = !EndDate
End With
Me.Recordsource = "SELECT * FROM MyTable WHERE DateField Between #" _
& dteStart & "# AND #" & dteEnd & "#;"
DoCmd.Close acForm, "dlgGetDates"
End If
End Sub
[Edit: IsLoaded() is a function provided by MS. It's so basic to all my Access coding I forget it's not a built-in function. The code:]
Function IsLoaded(ByVal strFormName As String) As Boolean
' Returns True if the specified form is open in Form view or Datasheet view.
Const conObjStateClosed = 0
Const conDesignView = 0
If SysCmd(acSysCmdGetObjectState, acForm, strFormName) <> conObjStateClosed Then
If Forms(strFormName).CurrentView <> conDesignView Then
IsLoaded = True
End If
End If
End Function
Now, there are certain advantages to this approach:
you can pre-fill the dates such that the user would have to click only OK.
you can re-use the form in multiple locations to collect date values for multiple forms, since the form doesn't care what report it's being used in.
However, there is no conditional way to choose whether to open the form or not.
Because of that, I often use a class module to store filtering criteria for reports, and then check if the relevant class module instance has filter values set at the time the OnOpen event fires. If the criteria are set, then I write the SQL for the recordsource on the fly, if they aren't set, I just use the default recordsource (and any filter passed in the OpenReport argument). In this setup, you would run the report only after you've collected the criteria and set up the class module instance. The advantage of this approach is that the report can be run in either of the two contexts, and none of the parts need to know anything about each other -- the report simply needs to understand the class module's interface.
I used to use the Docmd.Openreport Where clause, your option 2. But I've switched to using report filters in the reports Open event, your option 1, as it supports creating PDF files which the Where clause does not. See Microsoft Access Report Printing Criteria Selection Form at my website.
As far as the dates go in SQL strings I use the Format statement as at Return Dates in US #mm/dd/yyyy# format
Also your option three can be handled by putting in the following example in the criteria in the reports record source SQL query. Forms!Form_Name!Control_Name. This is generally simpler for folks who don't want to get into VBA.